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