@atproto/xrpc-server 0.11.5 → 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.
@@ -1,398 +0,0 @@
1
- import * as http from 'node:http'
2
- import { AddressInfo } from 'node:net'
3
- import { WebSocket, createWebSocketStream } from 'ws'
4
- import { wait } from '@atproto/common'
5
- import { LexiconDoc, Lexicons } from '@atproto/lexicon'
6
- import {
7
- ErrorFrame,
8
- Frame,
9
- MessageFrame,
10
- Subscription,
11
- byFrame,
12
- } from '../src/index.js'
13
- import * as xrpcServer from '../src/index.js'
14
- import {
15
- basicAuthHeaders,
16
- buildAddLexicons,
17
- buildMethodLexicons,
18
- closeServer,
19
- createBasicAuth,
20
- createServer,
21
- } from './_util.js'
22
-
23
- const LEXICONS = [
24
- {
25
- lexicon: 1,
26
- id: 'io.example.streamOne',
27
- defs: {
28
- main: {
29
- type: 'subscription',
30
- parameters: {
31
- type: 'params',
32
- required: ['countdown'],
33
- properties: {
34
- countdown: { type: 'integer' },
35
- },
36
- },
37
- message: {
38
- schema: { type: 'union', refs: ['#countdownStatus'] },
39
- },
40
- },
41
- countdownStatus: {
42
- type: 'object',
43
- required: ['count'],
44
- properties: { count: { type: 'integer' } },
45
- },
46
- },
47
- },
48
- {
49
- lexicon: 1,
50
- id: 'io.example.streamTwo',
51
- defs: {
52
- main: {
53
- type: 'subscription',
54
- parameters: {
55
- type: 'params',
56
- required: ['countdown'],
57
- properties: {
58
- countdown: { type: 'integer' },
59
- },
60
- },
61
- message: {
62
- schema: {
63
- type: 'union',
64
- refs: ['#even', '#odd'],
65
- },
66
- },
67
- },
68
- even: {
69
- type: 'object',
70
- required: ['count'],
71
- properties: { count: { type: 'integer' } },
72
- },
73
- odd: {
74
- type: 'object',
75
- required: ['count'],
76
- properties: { count: { type: 'integer' } },
77
- },
78
- },
79
- },
80
- {
81
- lexicon: 1,
82
- id: 'io.example.streamAuth',
83
- defs: {
84
- main: {
85
- type: 'subscription',
86
- message: {
87
- schema: { type: 'union', refs: ['#auth'] },
88
- },
89
- },
90
- auth: {
91
- type: 'object',
92
- properties: {
93
- credentials: { type: 'ref', ref: '#credentials' },
94
- artifacts: { type: 'ref', ref: '#artifacts' },
95
- },
96
- },
97
- credentials: {
98
- type: 'object',
99
- required: ['username'],
100
- properties: {
101
- username: { type: 'string' },
102
- },
103
- },
104
- artifacts: {
105
- type: 'object',
106
- required: ['original'],
107
- properties: {
108
- original: { type: 'string' },
109
- },
110
- },
111
- },
112
- },
113
- ] as const satisfies LexiconDoc[]
114
-
115
- const handlers = {
116
- 'io.example.streamOne': async function* ({
117
- params,
118
- }: xrpcServer.StreamContext) {
119
- const countdown = Number(params.countdown ?? 0)
120
- for (let i = countdown; i >= 0; i--) {
121
- await wait(0)
122
- yield { $type: 'io.example.streamOne#countdownStatus', count: i }
123
- }
124
- },
125
- 'io.example.streamTwo': async function* ({
126
- params,
127
- }: xrpcServer.StreamContext) {
128
- const countdown = Number(params.countdown ?? 0)
129
- for (let i = countdown; i >= 0; i--) {
130
- await wait(200)
131
- yield {
132
- $type: `io.example.streamTwo${i % 2 === 0 ? '#even' : '#odd'}`,
133
- count: i,
134
- }
135
- }
136
- yield {
137
- $type: 'io.example.otherNsid#done',
138
- }
139
- },
140
- 'io.example.streamAuth': {
141
- auth: createBasicAuth({ username: 'admin', password: 'password' }),
142
- handler: async function* ({ auth }) {
143
- yield { ...auth, $type: 'io.example.streamAuth#auth' }
144
- },
145
- },
146
- }
147
-
148
- for (const buildServer of [buildMethodLexicons, buildAddLexicons]) {
149
- describe(buildServer, () => {
150
- // @NOTE we need to clone because "new Lexicons" will mutate the lexicon
151
- // definitions
152
- const lex = new Lexicons(structuredClone(LEXICONS))
153
-
154
- let server: xrpcServer.Server
155
- let s: http.Server
156
- let port: number
157
- beforeAll(async () => {
158
- server = await buildServer(LEXICONS, handlers)
159
- s = await createServer(server)
160
- port = (s.address() as AddressInfo).port
161
- })
162
- afterAll(async () => {
163
- if (s) await closeServer(s)
164
- })
165
-
166
- it('streams messages', async () => {
167
- const ws = new WebSocket(
168
- `ws://localhost:${port}/xrpc/io.example.streamOne?countdown=5`,
169
- )
170
-
171
- const frames: Frame[] = []
172
- for await (const frame of byFrame(ws)) {
173
- frames.push(frame)
174
- }
175
-
176
- expect(frames).toEqual([
177
- new MessageFrame({ count: 5 }, { type: '#countdownStatus' }),
178
- new MessageFrame({ count: 4 }, { type: '#countdownStatus' }),
179
- new MessageFrame({ count: 3 }, { type: '#countdownStatus' }),
180
- new MessageFrame({ count: 2 }, { type: '#countdownStatus' }),
181
- new MessageFrame({ count: 1 }, { type: '#countdownStatus' }),
182
- new MessageFrame({ count: 0 }, { type: '#countdownStatus' }),
183
- ])
184
- })
185
-
186
- it('streams messages in a union', async () => {
187
- const ws = new WebSocket(
188
- `ws://localhost:${port}/xrpc/io.example.streamTwo?countdown=5`,
189
- )
190
-
191
- const frames: Frame[] = []
192
- for await (const frame of byFrame(ws)) {
193
- frames.push(frame)
194
- }
195
-
196
- expect(frames).toEqual([
197
- new MessageFrame({ count: 5 }, { type: '#odd' }),
198
- new MessageFrame({ count: 4 }, { type: '#even' }),
199
- new MessageFrame({ count: 3 }, { type: '#odd' }),
200
- new MessageFrame({ count: 2 }, { type: '#even' }),
201
- new MessageFrame({ count: 1 }, { type: '#odd' }),
202
- new MessageFrame({ count: 0 }, { type: '#even' }),
203
- new MessageFrame({}, { type: 'io.example.otherNsid#done' }),
204
- ])
205
- })
206
-
207
- it('resolves auth into handler', async () => {
208
- const ws = new WebSocket(
209
- `ws://localhost:${port}/xrpc/io.example.streamAuth`,
210
- {
211
- headers: basicAuthHeaders({
212
- username: 'admin',
213
- password: 'password',
214
- }),
215
- },
216
- )
217
-
218
- const frames: Frame[] = []
219
- for await (const frame of byFrame(ws)) {
220
- frames.push(frame)
221
- }
222
-
223
- expect(frames).toEqual([
224
- new MessageFrame(
225
- {
226
- credentials: {
227
- username: 'admin',
228
- },
229
- artifacts: {
230
- original: 'YWRtaW46cGFzc3dvcmQ=',
231
- },
232
- },
233
- {
234
- type: '#auth',
235
- },
236
- ),
237
- ])
238
- })
239
-
240
- it('errors immediately on bad parameter', async () => {
241
- const ws = new WebSocket(
242
- `ws://localhost:${port}/xrpc/io.example.streamOne`,
243
- )
244
-
245
- const frames: Frame[] = []
246
- for await (const frame of byFrame(ws)) {
247
- frames.push(frame)
248
- }
249
-
250
- expect(frames).toEqual([
251
- expect.objectContaining({
252
- body: expect.objectContaining({
253
- error: 'InvalidRequest',
254
- message: expect.stringContaining('countdown'),
255
- }),
256
- }),
257
- ])
258
- })
259
-
260
- it('errors immediately on bad auth', async () => {
261
- const ws = new WebSocket(
262
- `ws://localhost:${port}/xrpc/io.example.streamAuth`,
263
- {
264
- headers: basicAuthHeaders({
265
- username: 'bad',
266
- password: 'wrong',
267
- }),
268
- },
269
- )
270
-
271
- const frames: Frame[] = []
272
- for await (const frame of byFrame(ws)) {
273
- frames.push(frame)
274
- }
275
-
276
- expect(frames).toEqual([
277
- new ErrorFrame({
278
- error: 'AuthenticationRequired',
279
- message: 'Authentication Required',
280
- }),
281
- ])
282
- })
283
-
284
- it('does not websocket upgrade at bad endpoint', async () => {
285
- const ws = new WebSocket(`ws://localhost:${port}/xrpc/does.not.exist`)
286
- const drainStream = async () => {
287
- for await (const _bytes of createWebSocketStream(ws)) {
288
- // drain
289
- }
290
- }
291
- await expect(drainStream).rejects.toHaveProperty('code', 'ECONNRESET')
292
- })
293
-
294
- describe('Subscription consumer', () => {
295
- it('receives messages w/ skips', async () => {
296
- const sub = new Subscription({
297
- service: `ws://localhost:${port}`,
298
- method: 'io.example.streamOne',
299
- getParams: () => ({ countdown: 5 }),
300
- validate: (obj) => {
301
- const result = lex.assertValidXrpcMessage<{ count: number }>(
302
- 'io.example.streamOne',
303
- obj,
304
- )
305
- if (!result.count || result.count % 2) {
306
- return result
307
- }
308
- },
309
- })
310
-
311
- const messages: { count: number }[] = []
312
- for await (const msg of sub) {
313
- messages.push(msg)
314
- }
315
-
316
- expect(messages).toEqual([
317
- { $type: 'io.example.streamOne#countdownStatus', count: 5 },
318
- { $type: 'io.example.streamOne#countdownStatus', count: 3 },
319
- { $type: 'io.example.streamOne#countdownStatus', count: 1 },
320
- { $type: 'io.example.streamOne#countdownStatus', count: 0 },
321
- ])
322
- })
323
-
324
- it('reconnects w/ param update', async () => {
325
- let countdown = 10
326
- let reconnects = 0
327
- const sub = new Subscription({
328
- service: `ws://localhost:${port}`,
329
- method: 'io.example.streamOne',
330
- onReconnectError: () => reconnects++,
331
- getParams: () => ({ countdown }),
332
- validate: (obj) => {
333
- return lex.assertValidXrpcMessage<{ count: number }>(
334
- 'io.example.streamOne',
335
- obj,
336
- )
337
- },
338
- })
339
-
340
- let disconnected = false
341
- for await (const msg of sub) {
342
- expect(msg.count).toBeGreaterThanOrEqual(countdown - 1) // No skips
343
- countdown = Math.min(countdown, msg.count) // Only allow forward movement
344
- if (msg.count <= 6 && !disconnected) {
345
- disconnected = true
346
- server.subscriptions.forEach(({ wss }) => {
347
- wss.clients.forEach((c) => c.terminate())
348
- })
349
- }
350
- }
351
-
352
- expect(countdown).toEqual(0)
353
- expect(reconnects).toBeGreaterThan(0)
354
- })
355
-
356
- it('aborts with signal', async () => {
357
- const abortController = new AbortController()
358
- const sub = new Subscription({
359
- service: `ws://localhost:${port}`,
360
- method: 'io.example.streamOne',
361
- signal: abortController.signal,
362
- getParams: () => ({ countdown: 10 }),
363
- validate: (obj) => {
364
- const result = lex.assertValidXrpcMessage<{ count: number }>(
365
- 'io.example.streamOne',
366
- obj,
367
- )
368
- return result
369
- },
370
- })
371
-
372
- let error
373
- let disconnected = false
374
- const messages: { count: number }[] = []
375
- try {
376
- for await (const msg of sub) {
377
- messages.push(msg)
378
- if (msg.count <= 6 && !disconnected) {
379
- disconnected = true
380
- abortController.abort(new Error('Oops!'))
381
- }
382
- }
383
- } catch (err) {
384
- error = err
385
- }
386
-
387
- expect(error).toEqual(new Error('Oops!'))
388
- expect(messages).toEqual([
389
- { $type: 'io.example.streamOne#countdownStatus', count: 10 },
390
- { $type: 'io.example.streamOne#countdownStatus', count: 9 },
391
- { $type: 'io.example.streamOne#countdownStatus', count: 8 },
392
- { $type: 'io.example.streamOne#countdownStatus', count: 7 },
393
- { $type: 'io.example.streamOne#countdownStatus', count: 6 },
394
- ])
395
- })
396
- })
397
- })
398
- }
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "../../tsconfig/node.json",
3
- "compilerOptions": {
4
- "rootDir": "./src",
5
- "outDir": "./dist",
6
- },
7
- "include": ["./src"],
8
- }
@@ -1 +0,0 @@
1
- {"version":"7.0.0-dev.20260614.1","root":["./src/auth.ts","./src/errors.ts","./src/index.ts","./src/logger.ts","./src/rate-limiter-http.ts","./src/rate-limiter.ts","./src/server.ts","./src/types.ts","./src/util.ts","./src/stream/frames.ts","./src/stream/index.ts","./src/stream/logger.ts","./src/stream/server.ts","./src/stream/stream.ts","./src/stream/subscription.ts","./src/stream/types.ts"]}
package/tsconfig.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "include": [],
3
- "references": [
4
- { "path": "./tsconfig.build.json" },
5
- { "path": "./tsconfig.tests.json" },
6
- ],
7
- }
@@ -1,7 +0,0 @@
1
- {
2
- "extends": "../../tsconfig/tests.json",
3
- "compilerOptions": {
4
- "rootDir": ".",
5
- },
6
- "include": ["./tests"],
7
- }