@automerge/automerge-repo-network-websocket 1.1.0-alpha.1 → 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.
- package/dist/BrowserWebSocketClientAdapter.d.ts +8 -7
- package/dist/BrowserWebSocketClientAdapter.d.ts.map +1 -1
- package/dist/BrowserWebSocketClientAdapter.js +95 -92
- package/dist/NodeWSServerAdapter.d.ts +7 -5
- package/dist/NodeWSServerAdapter.d.ts.map +1 -1
- package/dist/NodeWSServerAdapter.js +107 -107
- package/dist/assert.d.ts +3 -0
- package/dist/assert.d.ts.map +1 -0
- package/dist/assert.js +17 -0
- package/dist/messages.d.ts +9 -11
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +5 -1
- package/dist/toArrayBuffer.d.ts +6 -0
- package/dist/toArrayBuffer.d.ts.map +1 -0
- package/dist/toArrayBuffer.js +8 -0
- package/package.json +4 -3
- package/src/BrowserWebSocketClientAdapter.ts +101 -123
- package/src/NodeWSServerAdapter.ts +121 -132
- package/src/assert.ts +28 -0
- package/src/messages.ts +23 -16
- package/src/toArrayBuffer.ts +8 -0
- package/test/Websocket.test.ts +353 -131
package/test/Websocket.test.ts
CHANGED
|
@@ -7,43 +7,32 @@ import {
|
|
|
7
7
|
SyncMessage,
|
|
8
8
|
parseAutomergeUrl,
|
|
9
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"
|
|
10
15
|
import assert from "assert"
|
|
11
16
|
import * as CBOR from "cbor-x"
|
|
12
17
|
import { once } from "events"
|
|
13
18
|
import http from "http"
|
|
19
|
+
import { getPortPromise as getAvailablePort } from "portfinder"
|
|
14
20
|
import { describe, it } from "vitest"
|
|
15
|
-
import WebSocket
|
|
16
|
-
import { runAdapterTests } from "../../automerge-repo/src/helpers/tests/network-adapter-tests.js"
|
|
17
|
-
import { DummyStorageAdapter } from "../../automerge-repo/test/helpers/DummyStorageAdapter.js"
|
|
21
|
+
import WebSocket from "ws"
|
|
18
22
|
import { BrowserWebSocketClientAdapter } from "../src/BrowserWebSocketClientAdapter.js"
|
|
19
23
|
import { NodeWSServerAdapter } from "../src/NodeWSServerAdapter.js"
|
|
20
|
-
import { headsAreSame } from "@automerge/automerge-repo/src/helpers/headsAreSame.js"
|
|
21
24
|
|
|
22
25
|
describe("Websocket adapters", () => {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
await new Promise<void>(resolve => server.listen(0, resolve))
|
|
28
|
-
const { port } = server.address() as AddressInfo
|
|
29
|
-
const serverUrl = `ws://localhost:${port}`
|
|
30
|
-
|
|
31
|
-
const clients = [] as BrowserWebSocketClientAdapter[]
|
|
32
|
-
for (let i = 0; i < clientCount; i++) {
|
|
33
|
-
clients.push(new BrowserWebSocketClientAdapter(serverUrl))
|
|
34
|
-
}
|
|
26
|
+
const browserPeerId = "browser" as PeerId
|
|
27
|
+
const serverPeerId = "server" as PeerId
|
|
28
|
+
const documentId = parseAutomergeUrl(generateAutomergeUrl()).documentId
|
|
35
29
|
|
|
36
|
-
return { socket, server, port, serverUrl, clients }
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// run adapter acceptance tests
|
|
40
30
|
runAdapterTests(async () => {
|
|
41
31
|
const {
|
|
42
32
|
clients: [aliceAdapter, bobAdapter],
|
|
43
|
-
socket,
|
|
44
33
|
server,
|
|
45
|
-
|
|
46
|
-
|
|
34
|
+
serverAdapter,
|
|
35
|
+
} = await setup({ clientCount: 2 })
|
|
47
36
|
|
|
48
37
|
const teardown = () => {
|
|
49
38
|
server.close()
|
|
@@ -53,63 +42,179 @@ describe("Websocket adapters", () => {
|
|
|
53
42
|
})
|
|
54
43
|
|
|
55
44
|
describe("BrowserWebSocketClientAdapter", () => {
|
|
56
|
-
const firstMessage = async (socket: WebSocket.Server<any>) =>
|
|
57
|
-
new Promise((resolve, reject) => {
|
|
58
|
-
socket.once("connection", ws => {
|
|
59
|
-
ws.once("message", (message: any) => resolve(message))
|
|
60
|
-
ws.once("error", (error: any) => reject(error))
|
|
61
|
-
})
|
|
62
|
-
socket.once("error", error => reject(error))
|
|
63
|
-
})
|
|
64
|
-
|
|
65
45
|
it("should advertise the protocol versions it supports in its join message", async () => {
|
|
66
46
|
const {
|
|
67
|
-
socket,
|
|
47
|
+
serverSocket: socket,
|
|
68
48
|
clients: [browser],
|
|
69
49
|
} = await setup()
|
|
70
50
|
|
|
71
|
-
const helloPromise =
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
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
|
+
})
|
|
76
55
|
})
|
|
77
56
|
|
|
57
|
+
const _repo = new Repo({ network: [browser], peerId: browserPeerId })
|
|
58
|
+
|
|
78
59
|
const encodedMessage = await helloPromise
|
|
79
60
|
const message = CBOR.decode(encodedMessage as Uint8Array)
|
|
80
61
|
assert.deepEqual(message, {
|
|
81
62
|
type: "join",
|
|
82
|
-
senderId:
|
|
83
|
-
storageId: undefined,
|
|
84
|
-
isEphemeral: true,
|
|
63
|
+
senderId: browserPeerId,
|
|
64
|
+
peerMetadata: { storageId: undefined, isEphemeral: true },
|
|
85
65
|
supportedProtocolVersions: ["1"],
|
|
86
66
|
})
|
|
87
67
|
})
|
|
88
68
|
|
|
89
|
-
it
|
|
69
|
+
it("should announce disconnections", async () => {
|
|
90
70
|
const {
|
|
91
|
-
|
|
92
|
-
socket,
|
|
71
|
+
serverAdapter,
|
|
93
72
|
clients: [browserAdapter],
|
|
94
73
|
} = await setup()
|
|
95
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
|
+
|
|
96
101
|
const _browserRepo = new Repo({
|
|
97
102
|
network: [browserAdapter],
|
|
98
|
-
peerId:
|
|
103
|
+
peerId: browserPeerId,
|
|
99
104
|
})
|
|
100
105
|
|
|
101
|
-
|
|
102
|
-
|
|
106
|
+
await pause(500)
|
|
107
|
+
|
|
108
|
+
const { serverAdapter } = await setupServer({ port, retryInterval })
|
|
109
|
+
const serverRepo = new Repo({
|
|
103
110
|
network: [serverAdapter],
|
|
104
|
-
peerId:
|
|
111
|
+
peerId: serverPeerId,
|
|
105
112
|
})
|
|
106
113
|
|
|
107
|
-
|
|
108
|
-
|
|
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,
|
|
109
190
|
})
|
|
110
191
|
|
|
111
|
-
server.
|
|
112
|
-
|
|
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)
|
|
113
218
|
})
|
|
114
219
|
|
|
115
220
|
it("should correctly clear event handlers on reconnect", async () => {
|
|
@@ -131,12 +236,12 @@ describe("Websocket adapters", () => {
|
|
|
131
236
|
} = await setup()
|
|
132
237
|
|
|
133
238
|
const peerId = "testclient" as PeerId
|
|
134
|
-
browser.connect(peerId
|
|
239
|
+
browser.connect(peerId)
|
|
135
240
|
|
|
136
241
|
// simulate the reconnect timer firing before the other end has responded
|
|
137
242
|
// (which works here because we haven't yielded to the event loop yet so
|
|
138
243
|
// the server, which is on the same event loop as us, can't respond)
|
|
139
|
-
browser.connect(peerId
|
|
244
|
+
browser.connect(peerId)
|
|
140
245
|
|
|
141
246
|
// Now yield, so the server responds on the first socket, if the listeners
|
|
142
247
|
// are cleaned up correctly we shouldn't throw
|
|
@@ -146,11 +251,13 @@ describe("Websocket adapters", () => {
|
|
|
146
251
|
|
|
147
252
|
describe("NodeWSServerAdapter", () => {
|
|
148
253
|
const serverResponse = async (clientHello: Object) => {
|
|
149
|
-
const {
|
|
150
|
-
|
|
254
|
+
const { serverSocket, serverUrl } = await setup({
|
|
255
|
+
clientCount: 0,
|
|
256
|
+
})
|
|
257
|
+
const server = new NodeWSServerAdapter(serverSocket)
|
|
151
258
|
const _serverRepo = new Repo({
|
|
152
259
|
network: [server],
|
|
153
|
-
peerId:
|
|
260
|
+
peerId: serverPeerId,
|
|
154
261
|
})
|
|
155
262
|
|
|
156
263
|
const clientSocket = new WebSocket(serverUrl)
|
|
@@ -164,7 +271,7 @@ describe("Websocket adapters", () => {
|
|
|
164
271
|
return message
|
|
165
272
|
}
|
|
166
273
|
|
|
167
|
-
async function
|
|
274
|
+
async function messageOrTimeout(socket: WebSocket): Promise<Buffer | null> {
|
|
168
275
|
return new Promise(resolve => {
|
|
169
276
|
const timer = setTimeout(() => {
|
|
170
277
|
resolve(null)
|
|
@@ -176,17 +283,75 @@ describe("Websocket adapters", () => {
|
|
|
176
283
|
})
|
|
177
284
|
}
|
|
178
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
|
+
|
|
179
345
|
it("should send the negotiated protocol version in its hello message", async () => {
|
|
180
346
|
const response = await serverResponse({
|
|
181
347
|
type: "join",
|
|
182
|
-
senderId:
|
|
348
|
+
senderId: browserPeerId,
|
|
183
349
|
supportedProtocolVersions: ["1"],
|
|
184
350
|
})
|
|
185
351
|
assert.deepEqual(response, {
|
|
186
352
|
type: "peer",
|
|
187
353
|
senderId: "server",
|
|
188
|
-
storageId: undefined,
|
|
189
|
-
isEphemeral: true,
|
|
354
|
+
peerMetadata: { storageId: undefined, isEphemeral: true },
|
|
190
355
|
targetId: "browser",
|
|
191
356
|
selectedProtocolVersion: "1",
|
|
192
357
|
})
|
|
@@ -195,7 +360,7 @@ describe("Websocket adapters", () => {
|
|
|
195
360
|
it("should return an error message if the protocol version is not supported", async () => {
|
|
196
361
|
const response = await serverResponse({
|
|
197
362
|
type: "join",
|
|
198
|
-
senderId:
|
|
363
|
+
senderId: browserPeerId,
|
|
199
364
|
supportedProtocolVersions: ["fake"],
|
|
200
365
|
})
|
|
201
366
|
assert.deepEqual(response, {
|
|
@@ -209,77 +374,17 @@ describe("Websocket adapters", () => {
|
|
|
209
374
|
it("should respond with protocol v1 if no protocol version is specified", async () => {
|
|
210
375
|
const response = await serverResponse({
|
|
211
376
|
type: "join",
|
|
212
|
-
senderId:
|
|
377
|
+
senderId: browserPeerId,
|
|
213
378
|
})
|
|
214
379
|
assert.deepEqual(response, {
|
|
215
380
|
type: "peer",
|
|
216
381
|
senderId: "server",
|
|
217
|
-
storageId: undefined,
|
|
218
|
-
isEphemeral: true,
|
|
382
|
+
peerMetadata: { storageId: undefined, isEphemeral: true },
|
|
219
383
|
targetId: "browser",
|
|
220
384
|
selectedProtocolVersion: "1",
|
|
221
385
|
})
|
|
222
386
|
})
|
|
223
387
|
|
|
224
|
-
/**
|
|
225
|
-
* Create a new document, initialized with the given contents and return a
|
|
226
|
-
* storage containign that document as well as the URL and a fork of the
|
|
227
|
-
* document
|
|
228
|
-
*
|
|
229
|
-
* @param contents - The contents to initialize the document with
|
|
230
|
-
*/
|
|
231
|
-
async function initDocAndStorage<T extends Record<string, unknown>>(
|
|
232
|
-
contents: T
|
|
233
|
-
): Promise<{
|
|
234
|
-
storage: DummyStorageAdapter
|
|
235
|
-
url: AutomergeUrl
|
|
236
|
-
doc: A.Doc<T>
|
|
237
|
-
documentId: DocumentId
|
|
238
|
-
}> {
|
|
239
|
-
const storage = new DummyStorageAdapter()
|
|
240
|
-
const silentRepo = new Repo({ storage, network: [] })
|
|
241
|
-
const doc = A.from<T>(contents)
|
|
242
|
-
const handle = silentRepo.create()
|
|
243
|
-
handle.update(() => A.clone(doc))
|
|
244
|
-
const { documentId } = parseAutomergeUrl(handle.url)
|
|
245
|
-
await pause(150)
|
|
246
|
-
return {
|
|
247
|
-
url: handle.url,
|
|
248
|
-
doc,
|
|
249
|
-
documentId,
|
|
250
|
-
storage,
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function assertIsPeerMessage(msg: Buffer | null) {
|
|
255
|
-
if (msg == null) {
|
|
256
|
-
throw new Error("expected a peer message, got null")
|
|
257
|
-
}
|
|
258
|
-
let decoded = CBOR.decode(msg)
|
|
259
|
-
if (decoded.type !== "peer") {
|
|
260
|
-
throw new Error(`expected a peer message, got type: ${decoded.type}`)
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function assertIsSyncMessage(
|
|
265
|
-
forDocument: DocumentId,
|
|
266
|
-
msg: Buffer | null
|
|
267
|
-
): SyncMessage {
|
|
268
|
-
if (msg == null) {
|
|
269
|
-
throw new Error("expected a peer message, got null")
|
|
270
|
-
}
|
|
271
|
-
let decoded = CBOR.decode(msg)
|
|
272
|
-
if (decoded.type !== "sync") {
|
|
273
|
-
throw new Error(`expected a peer message, got type: ${decoded.type}`)
|
|
274
|
-
}
|
|
275
|
-
if (decoded.documentId !== forDocument) {
|
|
276
|
-
throw new Error(
|
|
277
|
-
`expected a sync message for ${forDocument}, not for ${decoded.documentId}`
|
|
278
|
-
)
|
|
279
|
-
}
|
|
280
|
-
return decoded
|
|
281
|
-
}
|
|
282
|
-
|
|
283
388
|
it("should disconnect existing peers on reconnect before announcing them", async () => {
|
|
284
389
|
// This test exercises a sync loop which is exposed in the following
|
|
285
390
|
// sequence of events:
|
|
@@ -300,7 +405,67 @@ describe("Websocket adapters", () => {
|
|
|
300
405
|
// 6 and 7 continue in an infinite loop. The root cause is the servers
|
|
301
406
|
// failure to clear the sync state associated with the given peer when
|
|
302
407
|
// it receives a new connection from the same peer ID.
|
|
303
|
-
|
|
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()
|
|
304
469
|
|
|
305
470
|
// Create a doc, populate a DummyStorageAdapter with that doc
|
|
306
471
|
const { storage, url, doc, documentId } = await initDocAndStorage({
|
|
@@ -316,7 +481,7 @@ describe("Websocket adapters", () => {
|
|
|
316
481
|
const repo = new Repo({
|
|
317
482
|
network: [adapter],
|
|
318
483
|
storage,
|
|
319
|
-
peerId:
|
|
484
|
+
peerId: serverPeerId,
|
|
320
485
|
})
|
|
321
486
|
|
|
322
487
|
// make a change to the handle on the sync server
|
|
@@ -340,7 +505,7 @@ describe("Websocket adapters", () => {
|
|
|
340
505
|
})
|
|
341
506
|
)
|
|
342
507
|
|
|
343
|
-
let response = await
|
|
508
|
+
let response = await messageOrTimeout(clientSocket)
|
|
344
509
|
assertIsPeerMessage(response)
|
|
345
510
|
|
|
346
511
|
// Okay now we start syncing
|
|
@@ -363,7 +528,7 @@ describe("Websocket adapters", () => {
|
|
|
363
528
|
})
|
|
364
529
|
)
|
|
365
530
|
|
|
366
|
-
response = await
|
|
531
|
+
response = await messageOrTimeout(clientSocket)
|
|
367
532
|
assertIsSyncMessage(documentId, response)
|
|
368
533
|
|
|
369
534
|
// Now, assume either the network or the server is going slow, so the
|
|
@@ -388,7 +553,7 @@ describe("Websocket adapters", () => {
|
|
|
388
553
|
})
|
|
389
554
|
)
|
|
390
555
|
|
|
391
|
-
response = await
|
|
556
|
+
response = await messageOrTimeout(clientSocket)
|
|
392
557
|
assertIsPeerMessage(response)
|
|
393
558
|
|
|
394
559
|
// Now, we start syncing. If we're not buggy, this loop should terminate.
|
|
@@ -405,7 +570,7 @@ describe("Websocket adapters", () => {
|
|
|
405
570
|
})
|
|
406
571
|
)
|
|
407
572
|
}
|
|
408
|
-
const response = await
|
|
573
|
+
const response = await messageOrTimeout(clientSocket)
|
|
409
574
|
if (response) {
|
|
410
575
|
const decoded = assertIsSyncMessage(documentId, response)
|
|
411
576
|
;[clientDoc, clientState] = A.receiveSyncMessage(
|
|
@@ -430,5 +595,62 @@ describe("Websocket adapters", () => {
|
|
|
430
595
|
})
|
|
431
596
|
})
|
|
432
597
|
|
|
433
|
-
|
|
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) =>
|
|
434
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
|
+
}
|