@automerge/automerge-repo-network-websocket 1.0.19 → 1.1.0-alpha.13

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,42 +1,38 @@
1
1
  import { next as A } from "@automerge/automerge"
2
- import { AutomergeUrl, DocumentId, PeerId, Repo, SyncMessage, parseAutomergeUrl } from "@automerge/automerge-repo"
2
+ import {
3
+ AutomergeUrl,
4
+ DocumentId,
5
+ PeerId,
6
+ Repo,
7
+ SyncMessage,
8
+ parseAutomergeUrl,
9
+ } from "@automerge/automerge-repo"
10
+ import { generateAutomergeUrl } from "@automerge/automerge-repo/dist/AutomergeUrl"
11
+ import { eventPromise } from "@automerge/automerge-repo/src/helpers/eventPromise"
12
+ import { headsAreSame } from "@automerge/automerge-repo/src/helpers/headsAreSame.js"
13
+ import { runAdapterTests } from "@automerge/automerge-repo/src/helpers/tests/network-adapter-tests.js"
14
+ import { DummyStorageAdapter } from "@automerge/automerge-repo/test/helpers/DummyStorageAdapter.js"
3
15
  import assert from "assert"
4
16
  import * as CBOR from "cbor-x"
5
17
  import { once } from "events"
6
18
  import http from "http"
19
+ import { getPortPromise as getAvailablePort } from "portfinder"
7
20
  import { describe, it } from "vitest"
8
- import WebSocket, { AddressInfo } from "ws"
9
- import { runAdapterTests } from "../../automerge-repo/src/helpers/tests/network-adapter-tests.js"
10
- import { DummyStorageAdapter } from "../../automerge-repo/test/helpers/DummyStorageAdapter.js"
21
+ import WebSocket from "ws"
11
22
  import { BrowserWebSocketClientAdapter } from "../src/BrowserWebSocketClientAdapter.js"
12
23
  import { NodeWSServerAdapter } from "../src/NodeWSServerAdapter.js"
13
- import {headsAreSame} from "@automerge/automerge-repo/src/helpers/headsAreSame.js"
14
24
 
15
25
  describe("Websocket adapters", () => {
16
- const setup = async (clientCount = 1) => {
17
- const server = http.createServer()
18
- const socket = new WebSocket.Server({ server })
26
+ const browserPeerId = "browser" as PeerId
27
+ const serverPeerId = "server" as PeerId
28
+ const documentId = parseAutomergeUrl(generateAutomergeUrl()).documentId
19
29
 
20
- await new Promise<void>(resolve => server.listen(0, resolve))
21
- const { port } = server.address() as AddressInfo
22
- const serverUrl = `ws://localhost:${port}`
23
-
24
- const clients = [] as BrowserWebSocketClientAdapter[]
25
- for (let i = 0; i < clientCount; i++) {
26
- clients.push(new BrowserWebSocketClientAdapter(serverUrl))
27
- }
28
-
29
- return { socket, server, port, serverUrl, clients }
30
- }
31
-
32
- // run adapter acceptance tests
33
30
  runAdapterTests(async () => {
34
31
  const {
35
32
  clients: [aliceAdapter, bobAdapter],
36
- socket,
37
33
  server,
38
- } = await setup(2)
39
- const serverAdapter = new NodeWSServerAdapter(socket)
34
+ serverAdapter,
35
+ } = await setup({ clientCount: 2 })
40
36
 
41
37
  const teardown = () => {
42
38
  server.close()
@@ -46,61 +42,179 @@ describe("Websocket adapters", () => {
46
42
  })
47
43
 
48
44
  describe("BrowserWebSocketClientAdapter", () => {
49
- const firstMessage = async (socket: WebSocket.Server<any>) =>
50
- new Promise((resolve, reject) => {
51
- socket.once("connection", ws => {
52
- ws.once("message", (message: any) => resolve(message))
53
- ws.once("error", (error: any) => reject(error))
54
- })
55
- socket.once("error", error => reject(error))
56
- })
57
-
58
45
  it("should advertise the protocol versions it supports in its join message", async () => {
59
46
  const {
60
- socket,
47
+ serverSocket: socket,
61
48
  clients: [browser],
62
49
  } = await setup()
63
50
 
64
- const helloPromise = firstMessage(socket)
65
-
66
- const _repo = new Repo({
67
- network: [browser],
68
- peerId: "browser" as PeerId,
51
+ const helloPromise = new Promise((resolve, reject) => {
52
+ socket.once("connection", ws => {
53
+ ws.once("message", (message: any) => resolve(message))
54
+ })
69
55
  })
70
56
 
57
+ const _repo = new Repo({ network: [browser], peerId: browserPeerId })
58
+
71
59
  const encodedMessage = await helloPromise
72
60
  const message = CBOR.decode(encodedMessage as Uint8Array)
73
61
  assert.deepEqual(message, {
74
62
  type: "join",
75
- senderId: "browser",
63
+ senderId: browserPeerId,
64
+ peerMetadata: { storageId: undefined, isEphemeral: true },
76
65
  supportedProtocolVersions: ["1"],
77
66
  })
78
67
  })
79
68
 
80
- it.skip("should announce disconnections", async () => {
69
+ it("should announce disconnections", async () => {
81
70
  const {
82
- server,
83
- socket,
71
+ serverAdapter,
84
72
  clients: [browserAdapter],
85
73
  } = await setup()
86
74
 
75
+ const browserRepo = new Repo({
76
+ network: [browserAdapter],
77
+ peerId: browserPeerId,
78
+ })
79
+
80
+ const serverRepo = new Repo({
81
+ network: [serverAdapter],
82
+ peerId: serverPeerId,
83
+ })
84
+
85
+ await eventPromise(serverRepo.networkSubsystem, "peer")
86
+
87
+ browserAdapter.disconnect()
88
+
89
+ await Promise.all([
90
+ eventPromise(browserAdapter, "peer-disconnected"),
91
+ eventPromise(serverAdapter, "peer-disconnected"),
92
+ ])
93
+ })
94
+
95
+ it("should connect even when server is not initially available", async () => {
96
+ const port = await getPort() //?
97
+ const retryInterval = 100
98
+
99
+ const browserAdapter = await setupClient({ port, retryInterval })
100
+
87
101
  const _browserRepo = new Repo({
88
102
  network: [browserAdapter],
89
- peerId: "browser" as PeerId,
103
+ peerId: browserPeerId,
90
104
  })
91
105
 
92
- const serverAdapter = new NodeWSServerAdapter(socket)
93
- const _serverRepo = new Repo({
106
+ await pause(500)
107
+
108
+ const { serverAdapter } = await setupServer({ port, retryInterval })
109
+ const serverRepo = new Repo({
94
110
  network: [serverAdapter],
95
- peerId: "server" as PeerId,
111
+ peerId: serverPeerId,
96
112
  })
97
113
 
98
- const disconnectPromise = new Promise<void>(resolve => {
99
- browserAdapter.on("peer-disconnected", () => resolve())
114
+ await eventPromise(browserAdapter, "peer-candidate")
115
+ })
116
+
117
+ it("should reconnect after being disconnected", async () => {
118
+ const port = await getPort()
119
+ const retryInterval = 100
120
+
121
+ const browser = await setupClient({ port, retryInterval })
122
+
123
+ {
124
+ const { server, serverSocket, serverAdapter } = await setupServer({
125
+ port,
126
+ retryInterval,
127
+ })
128
+
129
+ const _browserRepo = new Repo({
130
+ network: [browser],
131
+ peerId: browserPeerId,
132
+ })
133
+
134
+ const serverRepo = new Repo({
135
+ network: [serverAdapter],
136
+ peerId: serverPeerId,
137
+ })
138
+
139
+ await eventPromise(browser, "peer-candidate")
140
+
141
+ // Stop the server
142
+ serverAdapter.disconnect()
143
+ server.close()
144
+ serverSocket.close()
145
+
146
+ await eventPromise(browser, "peer-disconnected")
147
+ }
148
+
149
+ {
150
+ // Restart the server (on the same port)
151
+ const { serverAdapter } = await setupServer({ port, retryInterval })
152
+
153
+ const serverRepo = new Repo({
154
+ network: [serverAdapter],
155
+ peerId: serverPeerId,
156
+ })
157
+
158
+ // The browserAdapter reconnects on its own
159
+ await eventPromise(browser, "peer-candidate")
160
+ }
161
+ })
162
+
163
+ it("should throw an error if asked to send a zero-length message", async () => {
164
+ const {
165
+ clients: [browser],
166
+ } = await setup()
167
+ const sendNoData = () => {
168
+ browser.send({
169
+ type: "sync",
170
+ data: new Uint8Array(), // <- empty
171
+ documentId,
172
+ senderId: browserPeerId,
173
+ targetId: serverPeerId,
174
+ })
175
+ }
176
+ assert.throws(sendNoData, /zero/)
177
+ })
178
+
179
+ it("should throw an error if asked to send before ready", async () => {
180
+ const port = await getPort()
181
+
182
+ const serverUrl = `ws://localhost:${port}`
183
+
184
+ const retry = 100
185
+ const browser = new BrowserWebSocketClientAdapter(serverUrl, retry)
186
+
187
+ const _browserRepo = new Repo({
188
+ network: [browser],
189
+ peerId: browserPeerId,
100
190
  })
101
191
 
102
- server.close()
103
- await disconnectPromise
192
+ const server = http.createServer()
193
+ const serverSocket = new WebSocket.Server({ server })
194
+
195
+ await new Promise<void>(resolve => server.listen(port, resolve))
196
+ const serverAdapter = new NodeWSServerAdapter(serverSocket, retry)
197
+
198
+ const serverRepo = new Repo({
199
+ network: [serverAdapter],
200
+ peerId: serverPeerId,
201
+ })
202
+
203
+ const sendMessage = () => {
204
+ browser.send({
205
+ // @ts-ignore
206
+ type: "foo",
207
+ data: new Uint8Array([1, 2, 3]),
208
+ documentId,
209
+ senderId: browserPeerId,
210
+ targetId: serverPeerId,
211
+ })
212
+ }
213
+ assert.throws(sendMessage, /not ready/)
214
+
215
+ // once the server is ready, we can send
216
+ await eventPromise(browser, "peer-candidate")
217
+ assert.doesNotThrow(sendMessage)
104
218
  })
105
219
 
106
220
  it("should correctly clear event handlers on reconnect", async () => {
@@ -137,11 +251,13 @@ describe("Websocket adapters", () => {
137
251
 
138
252
  describe("NodeWSServerAdapter", () => {
139
253
  const serverResponse = async (clientHello: Object) => {
140
- const { socket, serverUrl } = await setup(0)
141
- const server = new NodeWSServerAdapter(socket)
254
+ const { serverSocket, serverUrl } = await setup({
255
+ clientCount: 0,
256
+ })
257
+ const server = new NodeWSServerAdapter(serverSocket)
142
258
  const _serverRepo = new Repo({
143
259
  network: [server],
144
- peerId: "server" as PeerId,
260
+ peerId: serverPeerId,
145
261
  })
146
262
 
147
263
  const clientSocket = new WebSocket(serverUrl)
@@ -155,27 +271,87 @@ describe("Websocket adapters", () => {
155
271
  return message
156
272
  }
157
273
 
158
- async function recvOrTimeout(socket: WebSocket): Promise<Buffer | null> {
159
- return new Promise((resolve) => {
274
+ async function messageOrTimeout(socket: WebSocket): Promise<Buffer | null> {
275
+ return new Promise(resolve => {
160
276
  const timer = setTimeout(() => {
161
277
  resolve(null)
162
278
  }, 1000)
163
- socket.once("message", (msg) => {
279
+ socket.once("message", msg => {
164
280
  clearTimeout(timer)
165
281
  resolve(msg as Buffer)
166
282
  })
167
283
  })
168
284
  }
169
285
 
286
+ it("should disconnect from a closed client", async () => {
287
+ const {
288
+ serverAdapter,
289
+ clients: [browserAdapter],
290
+ } = await setup()
291
+
292
+ const _browserRepo = new Repo({
293
+ network: [browserAdapter],
294
+ peerId: browserPeerId,
295
+ })
296
+
297
+ const serverRepo = new Repo({
298
+ network: [serverAdapter],
299
+ peerId: serverPeerId,
300
+ })
301
+
302
+ await eventPromise(serverRepo.networkSubsystem, "peer")
303
+
304
+ const disconnectPromise = new Promise<void>(resolve => {
305
+ serverAdapter.on("peer-disconnected", () => resolve())
306
+ })
307
+
308
+ browserAdapter.socket!.close()
309
+
310
+ await disconnectPromise
311
+ })
312
+
313
+ it("should disconnect from a client that doesn't respond to pings", async () => {
314
+ const port = await getPort()
315
+
316
+ const serverUrl = `ws://localhost:${port}`
317
+
318
+ const retry = 100
319
+ const browserAdapter = new BrowserWebSocketClientAdapter(serverUrl, retry)
320
+
321
+ const server = http.createServer()
322
+ const serverSocket = new WebSocket.Server({ server })
323
+
324
+ await new Promise<void>(resolve => server.listen(port, resolve))
325
+ const serverAdapter = new NodeWSServerAdapter(serverSocket, retry)
326
+
327
+ const _browserRepo = new Repo({
328
+ network: [browserAdapter],
329
+ peerId: browserPeerId,
330
+ })
331
+
332
+ const serverRepo = new Repo({
333
+ network: [serverAdapter],
334
+ peerId: serverPeerId,
335
+ })
336
+
337
+ await eventPromise(serverAdapter, "peer-candidate")
338
+
339
+ // Simulate the client not responding to pings
340
+ browserAdapter.socket!.pong = () => {}
341
+
342
+ await eventPromise(serverAdapter, "peer-disconnected")
343
+ })
344
+
170
345
  it("should send the negotiated protocol version in its hello message", async () => {
171
346
  const response = await serverResponse({
172
347
  type: "join",
173
- senderId: "browser",
348
+ senderId: browserPeerId,
174
349
  supportedProtocolVersions: ["1"],
175
350
  })
176
351
  assert.deepEqual(response, {
177
352
  type: "peer",
178
353
  senderId: "server",
354
+ peerMetadata: { storageId: undefined, isEphemeral: true },
179
355
  targetId: "browser",
180
356
  selectedProtocolVersion: "1",
181
357
  })
@@ -184,7 +360,7 @@ describe("Websocket adapters", () => {
184
360
  it("should return an error message if the protocol version is not supported", async () => {
185
361
  const response = await serverResponse({
186
362
  type: "join",
187
- senderId: "browser",
363
+ senderId: browserPeerId,
188
364
  supportedProtocolVersions: ["fake"],
189
365
  })
190
366
  assert.deepEqual(response, {
@@ -198,70 +374,19 @@ describe("Websocket adapters", () => {
198
374
  it("should respond with protocol v1 if no protocol version is specified", async () => {
199
375
  const response = await serverResponse({
200
376
  type: "join",
201
- senderId: "browser",
377
+ senderId: browserPeerId,
202
378
  })
203
379
  assert.deepEqual(response, {
204
380
  type: "peer",
205
381
  senderId: "server",
382
+ peerMetadata: { storageId: undefined, isEphemeral: true },
206
383
  targetId: "browser",
207
384
  selectedProtocolVersion: "1",
208
385
  })
209
386
  })
210
387
 
211
- /**
212
- * Create a new document, initialized with the given contents and return a
213
- * storage containign that document as well as the URL and a fork of the
214
- * document
215
- *
216
- * @param contents - The contents to initialize the document with
217
- */
218
- async function initDocAndStorage<T extends Record<string, unknown>>(contents: T): Promise<{
219
- storage: DummyStorageAdapter,
220
- url: AutomergeUrl,
221
- doc: A.Doc<T>,
222
- documentId: DocumentId
223
- }> {
224
- const storage = new DummyStorageAdapter()
225
- const silentRepo = new Repo({storage, network: []})
226
- const doc = A.from<T>(contents)
227
- const handle = silentRepo.create()
228
- handle.update(() => A.clone(doc))
229
- const { documentId } = parseAutomergeUrl(handle.url)
230
- await pause(150)
231
- return {
232
- url: handle.url,
233
- doc,
234
- documentId,
235
- storage,
236
- }
237
- }
238
-
239
- function assertIsPeerMessage(msg: Buffer | null) {
240
- if (msg == null) {
241
- throw new Error("expected a peer message, got null")
242
- }
243
- let decoded = CBOR.decode(msg)
244
- if (decoded.type !== "peer") {
245
- throw new Error(`expected a peer message, got type: ${decoded.type}`)
246
- }
247
- }
248
-
249
- function assertIsSyncMessage(forDocument: DocumentId, msg: Buffer | null): SyncMessage {
250
- if (msg == null) {
251
- throw new Error("expected a peer message, got null")
252
- }
253
- let decoded = CBOR.decode(msg)
254
- if (decoded.type !== "sync") {
255
- throw new Error(`expected a peer message, got type: ${decoded.type}`)
256
- }
257
- if (decoded.documentId !== forDocument) {
258
- throw new Error(`expected a sync message for ${forDocument}, not for ${decoded.documentId}`)
259
- }
260
- return decoded
261
- }
262
-
263
388
  it("should disconnect existing peers on reconnect before announcing them", async () => {
264
- // This test exercises a sync loop which is exposed in the following
389
+ // This test exercises a sync loop which is exposed in the following
265
390
  // sequence of events:
266
391
  //
267
392
  // 1. A document exists on both the server and the client with divergent
@@ -276,27 +401,93 @@ describe("Websocket adapters", () => {
276
401
  // asks for them
277
402
  // 7. The server responds with an empty sync message because it thinks it
278
403
  // has already sent the changes
279
- //
404
+ //
280
405
  // 6 and 7 continue in an infinite loop. The root cause is the servers
281
406
  // failure to clear the sync state associated with the given peer when
282
407
  // it receives a new connection from the same peer ID.
283
- const { socket, serverUrl } = await setup(0)
408
+
409
+ /**
410
+ * Create a new document, initialized with the given contents and return a
411
+ * storage containing that document as well as the URL and a fork of the
412
+ * document
413
+ *
414
+ * @param contents - The contents to initialize the document with
415
+ */
416
+ async function initDocAndStorage<T extends Record<string, unknown>>(
417
+ contents: T
418
+ ): Promise<{
419
+ storage: DummyStorageAdapter
420
+ url: AutomergeUrl
421
+ doc: A.Doc<T>
422
+ documentId: DocumentId
423
+ }> {
424
+ const storage = new DummyStorageAdapter()
425
+ const silentRepo = new Repo({ storage, network: [] })
426
+ const doc = A.from<T>(contents)
427
+ const handle = silentRepo.create()
428
+ handle.update(() => A.clone(doc))
429
+ const { documentId } = parseAutomergeUrl(handle.url)
430
+ await pause(150)
431
+ return {
432
+ url: handle.url,
433
+ doc,
434
+ documentId,
435
+ storage,
436
+ }
437
+ }
438
+
439
+ function assertIsPeerMessage(msg: Buffer | null) {
440
+ if (msg == null) {
441
+ throw new Error("expected a peer message, got null")
442
+ }
443
+ let decoded = CBOR.decode(msg)
444
+ if (decoded.type !== "peer") {
445
+ throw new Error(`expected a peer message, got type: ${decoded.type}`)
446
+ }
447
+ }
448
+
449
+ function assertIsSyncMessage(
450
+ forDocument: DocumentId,
451
+ msg: Buffer | null
452
+ ): SyncMessage {
453
+ if (msg == null) {
454
+ throw new Error("expected a peer message, got null")
455
+ }
456
+ let decoded = CBOR.decode(msg)
457
+ if (decoded.type !== "sync") {
458
+ throw new Error(`expected a peer message, got type: ${decoded.type}`)
459
+ }
460
+ if (decoded.documentId !== forDocument) {
461
+ throw new Error(
462
+ `expected a sync message for ${forDocument}, not for ${decoded.documentId}`
463
+ )
464
+ }
465
+ return decoded
466
+ }
467
+
468
+ const { serverSocket: socket, serverUrl } = await setupServer()
284
469
 
285
470
  // Create a doc, populate a DummyStorageAdapter with that doc
286
- const {storage, url, doc, documentId} = await initDocAndStorage({foo: "bar"})
471
+ const { storage, url, doc, documentId } = await initDocAndStorage({
472
+ foo: "bar",
473
+ })
287
474
 
288
475
  // Create a copy of the document to represent the client state
289
- let clientDoc = A.clone<{foo: string}>(doc)
290
- clientDoc = A.change(clientDoc, d => d.foo = "qux")
476
+ let clientDoc = A.clone<{ foo: string }>(doc)
477
+ clientDoc = A.change(clientDoc, d => (d.foo = "qux"))
291
478
 
292
479
  // Now create a websocket sync server with the original document in it's storage
293
480
  const adapter = new NodeWSServerAdapter(socket)
294
- const repo = new Repo({ network: [adapter], storage, peerId: "server" as PeerId })
481
+ const repo = new Repo({
482
+ network: [adapter],
483
+ storage,
484
+ peerId: serverPeerId,
485
+ })
295
486
 
296
487
  // make a change to the handle on the sync server
297
- const handle = repo.find<{foo: string}>(url)
488
+ const handle = repo.find<{ foo: string }>(url)
298
489
  await handle.whenReady()
299
- handle.change(d => d.foo = "baz")
490
+ handle.change(d => (d.foo = "baz"))
300
491
 
301
492
  // Okay, so now there is a document on both the client and the server
302
493
  // which has concurrent changes on each peer.
@@ -306,36 +497,43 @@ describe("Websocket adapters", () => {
306
497
  await once(clientSocket, "open")
307
498
 
308
499
  // Run through the client/server hello
309
- clientSocket.send(CBOR.encode({
310
- type: "join",
311
- senderId: "client",
312
- supportedProtocolVersions: ["1"],
313
- }))
500
+ clientSocket.send(
501
+ CBOR.encode({
502
+ type: "join",
503
+ senderId: "client",
504
+ supportedProtocolVersions: ["1"],
505
+ })
506
+ )
314
507
 
315
- let response = await recvOrTimeout(clientSocket)
508
+ let response = await messageOrTimeout(clientSocket)
316
509
  assertIsPeerMessage(response)
317
510
 
318
511
  // Okay now we start syncing
319
512
 
320
513
  let clientState = A.initSyncState()
321
- let [newSyncState, message] = A.generateSyncMessage(clientDoc, clientState)
514
+ let [newSyncState, message] = A.generateSyncMessage(
515
+ clientDoc,
516
+ clientState
517
+ )
322
518
  clientState = newSyncState
323
519
 
324
520
  // Send the initial sync state
325
- clientSocket.send(CBOR.encode({
326
- type: "request",
327
- documentId,
328
- targetId: "server",
329
- senderId: "client",
330
- data: message
331
- }))
332
-
333
- response = await recvOrTimeout(clientSocket)
521
+ clientSocket.send(
522
+ CBOR.encode({
523
+ type: "request",
524
+ documentId,
525
+ targetId: "server",
526
+ senderId: "client",
527
+ data: message,
528
+ })
529
+ )
530
+
531
+ response = await messageOrTimeout(clientSocket)
334
532
  assertIsSyncMessage(documentId, response)
335
533
 
336
- // Now, assume either the network or the server is going slow, so the
534
+ // Now, assume either the network or the server is going slow, so the
337
535
  // server thinks it has sent the response above, but for whatever reason
338
- // it never gets to the client. In that case the reconnect timer in the
536
+ // it never gets to the client. In that case the reconnect timer in the
339
537
  // BrowserWebSocketClientAdapter will fire and we'll create a new
340
538
  // websocket and connect it. To simulate this we drop the above response
341
539
  // on the floor and start connecting again.
@@ -344,34 +542,42 @@ describe("Websocket adapters", () => {
344
542
  await once(clientSocket, "open")
345
543
 
346
544
  // and we also make a change to the client doc
347
- clientDoc = A.change(clientDoc, d => d.foo = "quoxen")
545
+ clientDoc = A.change(clientDoc, d => (d.foo = "quoxen"))
348
546
 
349
547
  // Run through the whole client/server hello dance again
350
- clientSocket.send(CBOR.encode({
351
- type: "join",
352
- senderId: "client",
353
- supportedProtocolVersions: ["1"],
354
- }))
548
+ clientSocket.send(
549
+ CBOR.encode({
550
+ type: "join",
551
+ senderId: "client",
552
+ supportedProtocolVersions: ["1"],
553
+ })
554
+ )
355
555
 
356
- response = await recvOrTimeout(clientSocket)
556
+ response = await messageOrTimeout(clientSocket)
357
557
  assertIsPeerMessage(response)
358
558
 
359
559
  // Now, we start syncing. If we're not buggy, this loop should terminate.
360
- while(true) {
560
+ while (true) {
361
561
  ;[clientState, message] = A.generateSyncMessage(clientDoc, clientState)
362
562
  if (message) {
363
- clientSocket.send(CBOR.encode({
364
- type: "sync",
365
- documentId,
366
- targetId: "server",
367
- senderId: "client",
368
- data: message
369
- }))
563
+ clientSocket.send(
564
+ CBOR.encode({
565
+ type: "sync",
566
+ documentId,
567
+ targetId: "server",
568
+ senderId: "client",
569
+ data: message,
570
+ })
571
+ )
370
572
  }
371
- const response = await recvOrTimeout(clientSocket)
573
+ const response = await messageOrTimeout(clientSocket)
372
574
  if (response) {
373
575
  const decoded = assertIsSyncMessage(documentId, response)
374
- ;[clientDoc, clientState] = A.receiveSyncMessage(clientDoc, clientState, decoded.data)
576
+ ;[clientDoc, clientState] = A.receiveSyncMessage(
577
+ clientDoc,
578
+ clientState,
579
+ decoded.data
580
+ )
375
581
  }
376
582
  if (response == null && message == null) {
377
583
  break
@@ -389,5 +595,62 @@ describe("Websocket adapters", () => {
389
595
  })
390
596
  })
391
597
 
392
- export const pause = (t = 0) =>
598
+ // HELPERS
599
+
600
+ const setup = async (options: SetupOptions = {}) => {
601
+ const {
602
+ clientCount = 1,
603
+ retryInterval = 1000,
604
+ port = await getPort(),
605
+ } = options
606
+
607
+ const { server, serverAdapter, serverSocket, serverUrl } = await setupServer(
608
+ options
609
+ )
610
+ const clients = await Promise.all(
611
+ Array.from({ length: clientCount }).map(() =>
612
+ setupClient({ retryInterval, port })
613
+ )
614
+ )
615
+ return { serverSocket, server, port, serverUrl, clients, serverAdapter }
616
+ }
617
+
618
+ const setupServer = async (options: SetupOptions = {}) => {
619
+ const {
620
+ clientCount = 1,
621
+ retryInterval = 1000,
622
+ port = await getPort(),
623
+ } = options
624
+ const serverUrl = `ws://localhost:${port}`
625
+ const server = http.createServer()
626
+ const serverSocket = new WebSocket.Server({ server })
627
+ await new Promise<void>(resolve => server.listen(port, resolve))
628
+ const serverAdapter = new NodeWSServerAdapter(serverSocket, retryInterval)
629
+ return { server, serverAdapter, serverSocket, serverUrl }
630
+ }
631
+
632
+ const setupClient = async (options: SetupOptions = {}) => {
633
+ const {
634
+ clientCount = 1,
635
+ retryInterval = 1000,
636
+ port = await getPort(),
637
+ } = options
638
+ const serverUrl = `ws://localhost:${port}`
639
+ return new BrowserWebSocketClientAdapter(serverUrl, retryInterval)
640
+ }
641
+
642
+ const pause = (t = 0) =>
393
643
  new Promise<void>(resolve => setTimeout(() => resolve(), t))
644
+
645
+ const getPort = () => {
646
+ const base = 3010
647
+ return getAvailablePort({ port: base })
648
+ }
649
+
650
+ // TYPES
651
+
652
+ type SetupOptions = {
653
+ clientCount?: number
654
+ retryInterval?: number
655
+ port?: number
656
+ }