@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.
- package/dist/BrowserWebSocketClientAdapter.d.ts +8 -7
- package/dist/BrowserWebSocketClientAdapter.d.ts.map +1 -1
- package/dist/BrowserWebSocketClientAdapter.js +96 -87
- package/dist/NodeWSServerAdapter.d.ts +7 -5
- package/dist/NodeWSServerAdapter.d.ts.map +1 -1
- package/dist/NodeWSServerAdapter.js +107 -96
- 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 -1
- 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 +108 -103
- package/src/NodeWSServerAdapter.ts +121 -116
- package/src/assert.ts +28 -0
- package/src/messages.ts +27 -4
- package/src/toArrayBuffer.ts +8 -0
- package/test/Websocket.test.ts +420 -157
package/test/Websocket.test.ts
CHANGED
|
@@ -1,42 +1,38 @@
|
|
|
1
1
|
import { next as A } from "@automerge/automerge"
|
|
2
|
-
import {
|
|
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
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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 =
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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:
|
|
63
|
+
senderId: browserPeerId,
|
|
64
|
+
peerMetadata: { storageId: undefined, isEphemeral: true },
|
|
76
65
|
supportedProtocolVersions: ["1"],
|
|
77
66
|
})
|
|
78
67
|
})
|
|
79
68
|
|
|
80
|
-
it
|
|
69
|
+
it("should announce disconnections", async () => {
|
|
81
70
|
const {
|
|
82
|
-
|
|
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:
|
|
103
|
+
peerId: browserPeerId,
|
|
90
104
|
})
|
|
91
105
|
|
|
92
|
-
|
|
93
|
-
|
|
106
|
+
await pause(500)
|
|
107
|
+
|
|
108
|
+
const { serverAdapter } = await setupServer({ port, retryInterval })
|
|
109
|
+
const serverRepo = new Repo({
|
|
94
110
|
network: [serverAdapter],
|
|
95
|
-
peerId:
|
|
111
|
+
peerId: serverPeerId,
|
|
96
112
|
})
|
|
97
113
|
|
|
98
|
-
|
|
99
|
-
|
|
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.
|
|
103
|
-
|
|
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 {
|
|
141
|
-
|
|
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:
|
|
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
|
|
159
|
-
return new Promise(
|
|
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",
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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({
|
|
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({
|
|
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(
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
500
|
+
clientSocket.send(
|
|
501
|
+
CBOR.encode({
|
|
502
|
+
type: "join",
|
|
503
|
+
senderId: "client",
|
|
504
|
+
supportedProtocolVersions: ["1"],
|
|
505
|
+
})
|
|
506
|
+
)
|
|
314
507
|
|
|
315
|
-
let response = await
|
|
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(
|
|
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(
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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(
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
548
|
+
clientSocket.send(
|
|
549
|
+
CBOR.encode({
|
|
550
|
+
type: "join",
|
|
551
|
+
senderId: "client",
|
|
552
|
+
supportedProtocolVersions: ["1"],
|
|
553
|
+
})
|
|
554
|
+
)
|
|
355
555
|
|
|
356
|
-
response = await
|
|
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(
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
|
573
|
+
const response = await messageOrTimeout(clientSocket)
|
|
372
574
|
if (response) {
|
|
373
575
|
const decoded = assertIsSyncMessage(documentId, response)
|
|
374
|
-
;[clientDoc, clientState] = A.receiveSyncMessage(
|
|
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
|
-
|
|
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
|
+
}
|