@automerge/automerge-repo-network-websocket 1.1.0-alpha.7 → 1.1.0
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 +91 -86
- package/dist/NodeWSServerAdapter.d.ts +5 -3
- package/dist/NodeWSServerAdapter.d.ts.map +1 -1
- package/dist/NodeWSServerAdapter.js +105 -102
- 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 +4 -0
- 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 +98 -106
- package/src/NodeWSServerAdapter.ts +118 -122
- package/src/assert.ts +28 -0
- package/src/messages.ts +18 -3
- package/src/toArrayBuffer.ts +8 -0
- package/test/Websocket.test.ts +350 -125
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,62 +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:
|
|
63
|
+
senderId: browserPeerId,
|
|
83
64
|
peerMetadata: { storageId: undefined, isEphemeral: true },
|
|
84
65
|
supportedProtocolVersions: ["1"],
|
|
85
66
|
})
|
|
86
67
|
})
|
|
87
68
|
|
|
88
|
-
it
|
|
69
|
+
it("should announce disconnections", async () => {
|
|
89
70
|
const {
|
|
90
|
-
|
|
91
|
-
socket,
|
|
71
|
+
serverAdapter,
|
|
92
72
|
clients: [browserAdapter],
|
|
93
73
|
} = await setup()
|
|
94
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
|
+
|
|
95
101
|
const _browserRepo = new Repo({
|
|
96
102
|
network: [browserAdapter],
|
|
97
|
-
peerId:
|
|
103
|
+
peerId: browserPeerId,
|
|
98
104
|
})
|
|
99
105
|
|
|
100
|
-
|
|
101
|
-
|
|
106
|
+
await pause(500)
|
|
107
|
+
|
|
108
|
+
const { serverAdapter } = await setupServer({ port, retryInterval })
|
|
109
|
+
const serverRepo = new Repo({
|
|
102
110
|
network: [serverAdapter],
|
|
103
|
-
peerId:
|
|
111
|
+
peerId: serverPeerId,
|
|
104
112
|
})
|
|
105
113
|
|
|
106
|
-
|
|
107
|
-
|
|
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,
|
|
108
190
|
})
|
|
109
191
|
|
|
110
|
-
server.
|
|
111
|
-
|
|
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)
|
|
112
218
|
})
|
|
113
219
|
|
|
114
220
|
it("should correctly clear event handlers on reconnect", async () => {
|
|
@@ -130,12 +236,12 @@ describe("Websocket adapters", () => {
|
|
|
130
236
|
} = await setup()
|
|
131
237
|
|
|
132
238
|
const peerId = "testclient" as PeerId
|
|
133
|
-
browser.connect(peerId
|
|
239
|
+
browser.connect(peerId)
|
|
134
240
|
|
|
135
241
|
// simulate the reconnect timer firing before the other end has responded
|
|
136
242
|
// (which works here because we haven't yielded to the event loop yet so
|
|
137
243
|
// the server, which is on the same event loop as us, can't respond)
|
|
138
|
-
browser.connect(peerId
|
|
244
|
+
browser.connect(peerId)
|
|
139
245
|
|
|
140
246
|
// Now yield, so the server responds on the first socket, if the listeners
|
|
141
247
|
// are cleaned up correctly we shouldn't throw
|
|
@@ -145,11 +251,13 @@ describe("Websocket adapters", () => {
|
|
|
145
251
|
|
|
146
252
|
describe("NodeWSServerAdapter", () => {
|
|
147
253
|
const serverResponse = async (clientHello: Object) => {
|
|
148
|
-
const {
|
|
149
|
-
|
|
254
|
+
const { serverSocket, serverUrl } = await setup({
|
|
255
|
+
clientCount: 0,
|
|
256
|
+
})
|
|
257
|
+
const server = new NodeWSServerAdapter(serverSocket)
|
|
150
258
|
const _serverRepo = new Repo({
|
|
151
259
|
network: [server],
|
|
152
|
-
peerId:
|
|
260
|
+
peerId: serverPeerId,
|
|
153
261
|
})
|
|
154
262
|
|
|
155
263
|
const clientSocket = new WebSocket(serverUrl)
|
|
@@ -163,7 +271,7 @@ describe("Websocket adapters", () => {
|
|
|
163
271
|
return message
|
|
164
272
|
}
|
|
165
273
|
|
|
166
|
-
async function
|
|
274
|
+
async function messageOrTimeout(socket: WebSocket): Promise<Buffer | null> {
|
|
167
275
|
return new Promise(resolve => {
|
|
168
276
|
const timer = setTimeout(() => {
|
|
169
277
|
resolve(null)
|
|
@@ -175,10 +283,69 @@ describe("Websocket adapters", () => {
|
|
|
175
283
|
})
|
|
176
284
|
}
|
|
177
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
|
+
|
|
178
345
|
it("should send the negotiated protocol version in its hello message", async () => {
|
|
179
346
|
const response = await serverResponse({
|
|
180
347
|
type: "join",
|
|
181
|
-
senderId:
|
|
348
|
+
senderId: browserPeerId,
|
|
182
349
|
supportedProtocolVersions: ["1"],
|
|
183
350
|
})
|
|
184
351
|
assert.deepEqual(response, {
|
|
@@ -193,7 +360,7 @@ describe("Websocket adapters", () => {
|
|
|
193
360
|
it("should return an error message if the protocol version is not supported", async () => {
|
|
194
361
|
const response = await serverResponse({
|
|
195
362
|
type: "join",
|
|
196
|
-
senderId:
|
|
363
|
+
senderId: browserPeerId,
|
|
197
364
|
supportedProtocolVersions: ["fake"],
|
|
198
365
|
})
|
|
199
366
|
assert.deepEqual(response, {
|
|
@@ -207,7 +374,7 @@ describe("Websocket adapters", () => {
|
|
|
207
374
|
it("should respond with protocol v1 if no protocol version is specified", async () => {
|
|
208
375
|
const response = await serverResponse({
|
|
209
376
|
type: "join",
|
|
210
|
-
senderId:
|
|
377
|
+
senderId: browserPeerId,
|
|
211
378
|
})
|
|
212
379
|
assert.deepEqual(response, {
|
|
213
380
|
type: "peer",
|
|
@@ -218,65 +385,6 @@ describe("Websocket adapters", () => {
|
|
|
218
385
|
})
|
|
219
386
|
})
|
|
220
387
|
|
|
221
|
-
/**
|
|
222
|
-
* Create a new document, initialized with the given contents and return a
|
|
223
|
-
* storage containign that document as well as the URL and a fork of the
|
|
224
|
-
* document
|
|
225
|
-
*
|
|
226
|
-
* @param contents - The contents to initialize the document with
|
|
227
|
-
*/
|
|
228
|
-
async function initDocAndStorage<T extends Record<string, unknown>>(
|
|
229
|
-
contents: T
|
|
230
|
-
): Promise<{
|
|
231
|
-
storage: DummyStorageAdapter
|
|
232
|
-
url: AutomergeUrl
|
|
233
|
-
doc: A.Doc<T>
|
|
234
|
-
documentId: DocumentId
|
|
235
|
-
}> {
|
|
236
|
-
const storage = new DummyStorageAdapter()
|
|
237
|
-
const silentRepo = new Repo({ storage, network: [] })
|
|
238
|
-
const doc = A.from<T>(contents)
|
|
239
|
-
const handle = silentRepo.create()
|
|
240
|
-
handle.update(() => A.clone(doc))
|
|
241
|
-
const { documentId } = parseAutomergeUrl(handle.url)
|
|
242
|
-
await pause(150)
|
|
243
|
-
return {
|
|
244
|
-
url: handle.url,
|
|
245
|
-
doc,
|
|
246
|
-
documentId,
|
|
247
|
-
storage,
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function assertIsPeerMessage(msg: Buffer | null) {
|
|
252
|
-
if (msg == null) {
|
|
253
|
-
throw new Error("expected a peer message, got null")
|
|
254
|
-
}
|
|
255
|
-
let decoded = CBOR.decode(msg)
|
|
256
|
-
if (decoded.type !== "peer") {
|
|
257
|
-
throw new Error(`expected a peer message, got type: ${decoded.type}`)
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function assertIsSyncMessage(
|
|
262
|
-
forDocument: DocumentId,
|
|
263
|
-
msg: Buffer | null
|
|
264
|
-
): SyncMessage {
|
|
265
|
-
if (msg == null) {
|
|
266
|
-
throw new Error("expected a peer message, got null")
|
|
267
|
-
}
|
|
268
|
-
let decoded = CBOR.decode(msg)
|
|
269
|
-
if (decoded.type !== "sync") {
|
|
270
|
-
throw new Error(`expected a peer message, got type: ${decoded.type}`)
|
|
271
|
-
}
|
|
272
|
-
if (decoded.documentId !== forDocument) {
|
|
273
|
-
throw new Error(
|
|
274
|
-
`expected a sync message for ${forDocument}, not for ${decoded.documentId}`
|
|
275
|
-
)
|
|
276
|
-
}
|
|
277
|
-
return decoded
|
|
278
|
-
}
|
|
279
|
-
|
|
280
388
|
it("should disconnect existing peers on reconnect before announcing them", async () => {
|
|
281
389
|
// This test exercises a sync loop which is exposed in the following
|
|
282
390
|
// sequence of events:
|
|
@@ -297,7 +405,67 @@ describe("Websocket adapters", () => {
|
|
|
297
405
|
// 6 and 7 continue in an infinite loop. The root cause is the servers
|
|
298
406
|
// failure to clear the sync state associated with the given peer when
|
|
299
407
|
// it receives a new connection from the same peer ID.
|
|
300
|
-
|
|
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()
|
|
301
469
|
|
|
302
470
|
// Create a doc, populate a DummyStorageAdapter with that doc
|
|
303
471
|
const { storage, url, doc, documentId } = await initDocAndStorage({
|
|
@@ -313,7 +481,7 @@ describe("Websocket adapters", () => {
|
|
|
313
481
|
const repo = new Repo({
|
|
314
482
|
network: [adapter],
|
|
315
483
|
storage,
|
|
316
|
-
peerId:
|
|
484
|
+
peerId: serverPeerId,
|
|
317
485
|
})
|
|
318
486
|
|
|
319
487
|
// make a change to the handle on the sync server
|
|
@@ -337,7 +505,7 @@ describe("Websocket adapters", () => {
|
|
|
337
505
|
})
|
|
338
506
|
)
|
|
339
507
|
|
|
340
|
-
let response = await
|
|
508
|
+
let response = await messageOrTimeout(clientSocket)
|
|
341
509
|
assertIsPeerMessage(response)
|
|
342
510
|
|
|
343
511
|
// Okay now we start syncing
|
|
@@ -360,7 +528,7 @@ describe("Websocket adapters", () => {
|
|
|
360
528
|
})
|
|
361
529
|
)
|
|
362
530
|
|
|
363
|
-
response = await
|
|
531
|
+
response = await messageOrTimeout(clientSocket)
|
|
364
532
|
assertIsSyncMessage(documentId, response)
|
|
365
533
|
|
|
366
534
|
// Now, assume either the network or the server is going slow, so the
|
|
@@ -385,7 +553,7 @@ describe("Websocket adapters", () => {
|
|
|
385
553
|
})
|
|
386
554
|
)
|
|
387
555
|
|
|
388
|
-
response = await
|
|
556
|
+
response = await messageOrTimeout(clientSocket)
|
|
389
557
|
assertIsPeerMessage(response)
|
|
390
558
|
|
|
391
559
|
// Now, we start syncing. If we're not buggy, this loop should terminate.
|
|
@@ -402,7 +570,7 @@ describe("Websocket adapters", () => {
|
|
|
402
570
|
})
|
|
403
571
|
)
|
|
404
572
|
}
|
|
405
|
-
const response = await
|
|
573
|
+
const response = await messageOrTimeout(clientSocket)
|
|
406
574
|
if (response) {
|
|
407
575
|
const decoded = assertIsSyncMessage(documentId, response)
|
|
408
576
|
;[clientDoc, clientState] = A.receiveSyncMessage(
|
|
@@ -427,5 +595,62 @@ describe("Websocket adapters", () => {
|
|
|
427
595
|
})
|
|
428
596
|
})
|
|
429
597
|
|
|
430
|
-
|
|
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) =>
|
|
431
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
|
+
}
|