@automerge/automerge-repo-network-websocket 1.0.8 → 1.0.10
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 +4 -0
- package/dist/BrowserWebSocketClientAdapter.d.ts.map +1 -1
- package/dist/BrowserWebSocketClientAdapter.js +30 -27
- package/dist/NodeWSServerAdapter.js +1 -1
- package/package.json +3 -3
- package/src/BrowserWebSocketClientAdapter.ts +35 -34
- package/src/NodeWSServerAdapter.ts +1 -1
- package/test/Websocket.test.ts +166 -113
- package/test/utilities/CreateWebSocketServer.ts +0 -8
- package/test/utilities/WebSockets.ts +0 -34
|
@@ -8,9 +8,13 @@ declare abstract class WebSocketNetworkAdapter extends NetworkAdapter {
|
|
|
8
8
|
export declare class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
9
9
|
#private;
|
|
10
10
|
timerId?: ReturnType<typeof setTimeout>;
|
|
11
|
+
remotePeerId?: PeerId;
|
|
11
12
|
url: string;
|
|
12
13
|
constructor(url: string);
|
|
13
14
|
connect(peerId: PeerId): void;
|
|
15
|
+
onOpen: () => void;
|
|
16
|
+
onClose: () => void;
|
|
17
|
+
onMessage: (event: WebSocket.MessageEvent) => void;
|
|
14
18
|
join(): void;
|
|
15
19
|
disconnect(): void;
|
|
16
20
|
send(message: FromClientMessage): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BrowserWebSocketClientAdapter.d.ts","sourceRoot":"","sources":["../src/BrowserWebSocketClientAdapter.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,EAAQ,MAAM,2BAA2B,CAAA;AACxE,OAAO,SAAS,MAAM,eAAe,CAAA;AAIrC,OAAO,EACL,iBAAiB,EAGlB,MAAM,eAAe,CAAA;AAMtB,uBAAe,uBAAwB,SAAQ,cAAc;IAC3D,MAAM,CAAC,EAAE,SAAS,CAAA;CACnB;AAED,qBAAa,6BAA8B,SAAQ,uBAAuB;;IAGxE,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"BrowserWebSocketClientAdapter.d.ts","sourceRoot":"","sources":["../src/BrowserWebSocketClientAdapter.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,EAAQ,MAAM,2BAA2B,CAAA;AACxE,OAAO,SAAS,MAAM,eAAe,CAAA;AAIrC,OAAO,EACL,iBAAiB,EAGlB,MAAM,eAAe,CAAA;AAMtB,uBAAe,uBAAwB,SAAQ,cAAc;IAC3D,MAAM,CAAC,EAAE,SAAS,CAAA;CACnB;AAED,qBAAa,6BAA8B,SAAQ,uBAAuB;;IAGxE,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAA;IACvC,YAAY,CAAC,EAAE,MAAM,CAAA;IAGrB,GAAG,EAAE,MAAM,CAAA;gBAEC,GAAG,EAAE,MAAM;IAKvB,OAAO,CAAC,MAAM,EAAE,MAAM;IAkCtB,MAAM,aAKL;IAGD,OAAO,aAYN;IAED,SAAS,UAAW,sBAAsB,UAEzC;IAED,IAAI;IAWJ,UAAU;IAOV,IAAI,CAAC,OAAO,EAAE,iBAAiB;IAwB/B,kBAAkB,CAAC,MAAM,EAAE,MAAM;IAcjC,cAAc,CAAC,OAAO,EAAE,UAAU;CA6BnC"}
|
|
@@ -11,6 +11,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
11
11
|
// Type trickery required for platform independence,
|
|
12
12
|
// see https://stackoverflow.com/questions/45802988/typescript-use-correct-version-of-settimeout-node-vs-window
|
|
13
13
|
timerId;
|
|
14
|
+
remotePeerId; // this adapter only connects to one remote client at a time
|
|
14
15
|
#startupComplete = false;
|
|
15
16
|
url;
|
|
16
17
|
constructor(url) {
|
|
@@ -18,27 +19,12 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
18
19
|
this.url = url;
|
|
19
20
|
}
|
|
20
21
|
connect(peerId) {
|
|
21
|
-
const onOpen = () => {
|
|
22
|
-
log(`@ ${this.url}: open`);
|
|
23
|
-
clearInterval(this.timerId);
|
|
24
|
-
this.timerId = undefined;
|
|
25
|
-
};
|
|
26
|
-
// When a socket closes, or disconnects, remove it from the array.
|
|
27
|
-
const onClose = () => {
|
|
28
|
-
log(`${this.url}: close`);
|
|
29
|
-
if (!this.timerId) {
|
|
30
|
-
this.connect(peerId);
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
const onMessage = (event) => {
|
|
34
|
-
this.receiveMessage(event.data);
|
|
35
|
-
};
|
|
36
22
|
// If we're reconnecting make sure we remove the old event listeners
|
|
37
23
|
// before creating a new connection.
|
|
38
24
|
if (this.socket) {
|
|
39
|
-
this.socket.removeEventListener("open", onOpen);
|
|
40
|
-
this.socket.removeEventListener("close", onClose);
|
|
41
|
-
this.socket.removeEventListener("message", onMessage);
|
|
25
|
+
this.socket.removeEventListener("open", this.onOpen);
|
|
26
|
+
this.socket.removeEventListener("close", this.onClose);
|
|
27
|
+
this.socket.removeEventListener("message", this.onMessage);
|
|
42
28
|
}
|
|
43
29
|
if (!this.timerId) {
|
|
44
30
|
this.timerId = setInterval(() => this.connect(peerId), 5000);
|
|
@@ -46,9 +32,9 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
46
32
|
this.peerId = peerId;
|
|
47
33
|
this.socket = new WebSocket(this.url);
|
|
48
34
|
this.socket.binaryType = "arraybuffer";
|
|
49
|
-
this.socket.addEventListener("open", onOpen);
|
|
50
|
-
this.socket.addEventListener("close", onClose);
|
|
51
|
-
this.socket.addEventListener("message", onMessage);
|
|
35
|
+
this.socket.addEventListener("open", this.onOpen);
|
|
36
|
+
this.socket.addEventListener("close", this.onClose);
|
|
37
|
+
this.socket.addEventListener("message", this.onMessage);
|
|
52
38
|
// mark this adapter as ready if we haven't received an ack in 1 second.
|
|
53
39
|
// We might hear back from the other end at some point but we shouldn't
|
|
54
40
|
// hold up marking things as unavailable for any longer
|
|
@@ -60,6 +46,27 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
60
46
|
}, 1000);
|
|
61
47
|
this.join();
|
|
62
48
|
}
|
|
49
|
+
onOpen = () => {
|
|
50
|
+
log(`@ ${this.url}: open`);
|
|
51
|
+
clearInterval(this.timerId);
|
|
52
|
+
this.timerId = undefined;
|
|
53
|
+
this.send(joinMessage(this.peerId));
|
|
54
|
+
};
|
|
55
|
+
// When a socket closes, or disconnects, remove it from the array.
|
|
56
|
+
onClose = () => {
|
|
57
|
+
log(`${this.url}: close`);
|
|
58
|
+
if (this.remotePeerId) {
|
|
59
|
+
this.emit("peer-disconnected", { peerId: this.remotePeerId });
|
|
60
|
+
}
|
|
61
|
+
if (!this.timerId) {
|
|
62
|
+
if (this.peerId) {
|
|
63
|
+
this.connect(this.peerId);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
onMessage = (event) => {
|
|
68
|
+
this.receiveMessage(event.data);
|
|
69
|
+
};
|
|
63
70
|
join() {
|
|
64
71
|
if (!this.socket) {
|
|
65
72
|
throw new Error("WTF, get a socket");
|
|
@@ -68,12 +75,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
68
75
|
this.send(joinMessage(this.peerId));
|
|
69
76
|
}
|
|
70
77
|
else {
|
|
71
|
-
|
|
72
|
-
if (!this.socket) {
|
|
73
|
-
throw new Error("WTF, get a socket");
|
|
74
|
-
}
|
|
75
|
-
this.send(joinMessage(this.peerId));
|
|
76
|
-
}, { once: true });
|
|
78
|
+
// The onOpen handler automatically sends a join message
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
81
|
disconnect() {
|
|
@@ -108,6 +110,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
108
110
|
this.#startupComplete = true;
|
|
109
111
|
this.emit("ready", { network: this });
|
|
110
112
|
}
|
|
113
|
+
this.remotePeerId = peerId;
|
|
111
114
|
this.emit("peer-candidate", { peerId });
|
|
112
115
|
}
|
|
113
116
|
receiveMessage(message) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automerge/automerge-repo-network-websocket",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "isomorphic node/browser Websocket network adapter for Automerge Repo",
|
|
5
5
|
"peerDependencies": {
|
|
6
6
|
"@automerge/automerge": "^2.1.0"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"test": "vitest"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@automerge/automerge-repo": "^1.0.
|
|
19
|
+
"@automerge/automerge-repo": "^1.0.10",
|
|
20
20
|
"cbor-x": "^1.3.0",
|
|
21
21
|
"eventemitter3": "^5.0.1",
|
|
22
22
|
"isomorphic-ws": "^5.0.0",
|
|
@@ -33,5 +33,5 @@
|
|
|
33
33
|
"publishConfig": {
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
|
-
"gitHead": "
|
|
36
|
+
"gitHead": "a79cfa659547b7f4a12987bbbee0f4f2f7f219f4"
|
|
37
37
|
}
|
|
@@ -21,6 +21,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
21
21
|
// Type trickery required for platform independence,
|
|
22
22
|
// see https://stackoverflow.com/questions/45802988/typescript-use-correct-version-of-settimeout-node-vs-window
|
|
23
23
|
timerId?: ReturnType<typeof setTimeout>
|
|
24
|
+
remotePeerId?: PeerId // this adapter only connects to one remote client at a time
|
|
24
25
|
#startupComplete: boolean = false
|
|
25
26
|
|
|
26
27
|
url: string
|
|
@@ -31,30 +32,12 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
connect(peerId: PeerId) {
|
|
34
|
-
const onOpen = () => {
|
|
35
|
-
log(`@ ${this.url}: open`)
|
|
36
|
-
clearInterval(this.timerId)
|
|
37
|
-
this.timerId = undefined
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// When a socket closes, or disconnects, remove it from the array.
|
|
41
|
-
const onClose = () => {
|
|
42
|
-
log(`${this.url}: close`)
|
|
43
|
-
if (!this.timerId) {
|
|
44
|
-
this.connect(peerId)
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const onMessage = (event: WebSocket.MessageEvent) => {
|
|
49
|
-
this.receiveMessage(event.data as Uint8Array)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
35
|
// If we're reconnecting make sure we remove the old event listeners
|
|
53
36
|
// before creating a new connection.
|
|
54
37
|
if (this.socket) {
|
|
55
|
-
this.socket.removeEventListener("open", onOpen)
|
|
56
|
-
this.socket.removeEventListener("close", onClose)
|
|
57
|
-
this.socket.removeEventListener("message", onMessage)
|
|
38
|
+
this.socket.removeEventListener("open", this.onOpen)
|
|
39
|
+
this.socket.removeEventListener("close", this.onClose)
|
|
40
|
+
this.socket.removeEventListener("message", this.onMessage)
|
|
58
41
|
}
|
|
59
42
|
|
|
60
43
|
if (!this.timerId) {
|
|
@@ -65,9 +48,9 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
65
48
|
this.socket = new WebSocket(this.url)
|
|
66
49
|
this.socket.binaryType = "arraybuffer"
|
|
67
50
|
|
|
68
|
-
this.socket.addEventListener("open", onOpen)
|
|
69
|
-
this.socket.addEventListener("close", onClose)
|
|
70
|
-
this.socket.addEventListener("message", onMessage)
|
|
51
|
+
this.socket.addEventListener("open", this.onOpen)
|
|
52
|
+
this.socket.addEventListener("close", this.onClose)
|
|
53
|
+
this.socket.addEventListener("message", this.onMessage)
|
|
71
54
|
|
|
72
55
|
// mark this adapter as ready if we haven't received an ack in 1 second.
|
|
73
56
|
// We might hear back from the other end at some point but we shouldn't
|
|
@@ -82,6 +65,32 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
82
65
|
this.join()
|
|
83
66
|
}
|
|
84
67
|
|
|
68
|
+
onOpen = () => {
|
|
69
|
+
log(`@ ${this.url}: open`)
|
|
70
|
+
clearInterval(this.timerId)
|
|
71
|
+
this.timerId = undefined
|
|
72
|
+
this.send(joinMessage(this.peerId!))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// When a socket closes, or disconnects, remove it from the array.
|
|
76
|
+
onClose = () => {
|
|
77
|
+
log(`${this.url}: close`)
|
|
78
|
+
|
|
79
|
+
if (this.remotePeerId) {
|
|
80
|
+
this.emit("peer-disconnected", { peerId: this.remotePeerId })
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!this.timerId) {
|
|
84
|
+
if (this.peerId) {
|
|
85
|
+
this.connect(this.peerId)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
onMessage = (event: WebSocket.MessageEvent) => {
|
|
91
|
+
this.receiveMessage(event.data as Uint8Array)
|
|
92
|
+
}
|
|
93
|
+
|
|
85
94
|
join() {
|
|
86
95
|
if (!this.socket) {
|
|
87
96
|
throw new Error("WTF, get a socket")
|
|
@@ -89,16 +98,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
89
98
|
if (this.socket.readyState === WebSocket.OPEN) {
|
|
90
99
|
this.send(joinMessage(this.peerId!))
|
|
91
100
|
} else {
|
|
92
|
-
|
|
93
|
-
"open",
|
|
94
|
-
() => {
|
|
95
|
-
if (!this.socket) {
|
|
96
|
-
throw new Error("WTF, get a socket")
|
|
97
|
-
}
|
|
98
|
-
this.send(joinMessage(this.peerId!))
|
|
99
|
-
},
|
|
100
|
-
{ once: true }
|
|
101
|
-
)
|
|
101
|
+
// The onOpen handler automatically sends a join message
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
@@ -143,6 +143,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
143
143
|
this.#startupComplete = true
|
|
144
144
|
this.emit("ready", { network: this })
|
|
145
145
|
}
|
|
146
|
+
this.remotePeerId = peerId
|
|
146
147
|
this.emit("peer-candidate", { peerId })
|
|
147
148
|
}
|
|
148
149
|
|
package/test/Websocket.test.ts
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
import { runAdapterTests } from "../../automerge-repo/src/helpers/tests/network-adapter-tests.js"
|
|
2
|
-
import { BrowserWebSocketClientAdapter } from "../src/BrowserWebSocketClientAdapter.js"
|
|
3
|
-
import { NodeWSServerAdapter } from "../src/NodeWSServerAdapter.js"
|
|
4
|
-
import { startServer } from "./utilities/WebSockets.js"
|
|
5
|
-
import * as CBOR from "cbor-x"
|
|
6
|
-
import WebSocket, { AddressInfo } from "ws"
|
|
7
|
-
import assert from "assert"
|
|
8
1
|
import { PeerId, Repo } from "@automerge/automerge-repo"
|
|
2
|
+
import assert from "assert"
|
|
3
|
+
import * as CBOR from "cbor-x"
|
|
9
4
|
import { once } from "events"
|
|
5
|
+
import http from "http"
|
|
10
6
|
import { describe, it } from "vitest"
|
|
7
|
+
import WebSocket, { AddressInfo } from "ws"
|
|
8
|
+
import { runAdapterTests } from "../../automerge-repo/src/helpers/tests/network-adapter-tests.js"
|
|
9
|
+
import { BrowserWebSocketClientAdapter } from "../src/BrowserWebSocketClientAdapter.js"
|
|
10
|
+
import { NodeWSServerAdapter } from "../src/NodeWSServerAdapter.js"
|
|
11
11
|
|
|
12
|
-
describe("Websocket adapters",
|
|
13
|
-
|
|
12
|
+
describe("Websocket adapters", () => {
|
|
13
|
+
const setup = async (clientCount = 1) => {
|
|
14
|
+
const server = http.createServer()
|
|
15
|
+
const socket = new WebSocket.Server({ server })
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
while (socket === undefined) {
|
|
20
|
-
try {
|
|
21
|
-
;({ socket, server } = await startServer(port))
|
|
22
|
-
} catch (e: any) {
|
|
23
|
-
if (e.code === "EADDRINUSE") {
|
|
24
|
-
port++
|
|
25
|
-
} else {
|
|
26
|
-
throw e
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
17
|
+
await new Promise<void>(resolve => server.listen(0, resolve))
|
|
18
|
+
const { port } = server.address() as AddressInfo
|
|
19
|
+
const serverUrl = `ws://localhost:${port}`
|
|
30
20
|
|
|
31
|
-
const
|
|
21
|
+
const clients = [] as BrowserWebSocketClientAdapter[]
|
|
22
|
+
for (let i = 0; i < clientCount; i++) {
|
|
23
|
+
clients.push(new BrowserWebSocketClientAdapter(serverUrl))
|
|
24
|
+
}
|
|
32
25
|
|
|
33
|
-
|
|
26
|
+
return { socket, server, port, serverUrl, clients }
|
|
27
|
+
}
|
|
34
28
|
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
// run adapter acceptance tests
|
|
30
|
+
runAdapterTests(async () => {
|
|
31
|
+
const {
|
|
32
|
+
clients: [aliceAdapter, bobAdapter],
|
|
33
|
+
socket,
|
|
34
|
+
server,
|
|
35
|
+
} = await setup(2)
|
|
36
|
+
const serverAdapter = new NodeWSServerAdapter(socket)
|
|
37
37
|
|
|
38
38
|
const teardown = () => {
|
|
39
39
|
server.close()
|
|
@@ -41,106 +41,159 @@ describe("Websocket adapters", async () => {
|
|
|
41
41
|
|
|
42
42
|
return { adapters: [aliceAdapter, serverAdapter, bobAdapter], teardown }
|
|
43
43
|
})
|
|
44
|
-
})
|
|
45
44
|
|
|
46
|
-
describe("
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
describe("BrowserWebSocketClientAdapter", () => {
|
|
46
|
+
const firstMessage = async (socket: WebSocket.Server<any>) =>
|
|
47
|
+
new Promise((resolve, reject) => {
|
|
48
|
+
socket.once("connection", ws => {
|
|
49
|
+
ws.once("message", (message: any) => resolve(message))
|
|
50
|
+
ws.once("error", (error: any) => reject(error))
|
|
51
|
+
})
|
|
52
|
+
socket.once("error", error => reject(error))
|
|
53
|
+
})
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
it("should advertise the protocol versions it supports in its join message", async () => {
|
|
56
|
+
const {
|
|
57
|
+
socket,
|
|
58
|
+
clients: [browser],
|
|
59
|
+
} = await setup()
|
|
55
60
|
|
|
56
|
-
|
|
61
|
+
const helloPromise = firstMessage(socket)
|
|
57
62
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
supportedProtocolVersions: ["1"],
|
|
63
|
-
})
|
|
64
|
-
})
|
|
65
|
-
})
|
|
63
|
+
const _repo = new Repo({
|
|
64
|
+
network: [browser],
|
|
65
|
+
peerId: "browser" as PeerId,
|
|
66
|
+
})
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
assert.deepEqual<any>(response, {
|
|
75
|
-
type: "peer",
|
|
76
|
-
senderId: "server",
|
|
77
|
-
targetId: "browser",
|
|
78
|
-
selectedProtocolVersion: "1",
|
|
68
|
+
const encodedMessage = await helloPromise
|
|
69
|
+
const message = CBOR.decode(encodedMessage as Uint8Array)
|
|
70
|
+
assert.deepEqual(message, {
|
|
71
|
+
type: "join",
|
|
72
|
+
senderId: "browser",
|
|
73
|
+
supportedProtocolVersions: ["1"],
|
|
74
|
+
})
|
|
79
75
|
})
|
|
80
|
-
})
|
|
81
76
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
77
|
+
it.skip("should announce disconnections", async () => {
|
|
78
|
+
const {
|
|
79
|
+
server,
|
|
80
|
+
socket,
|
|
81
|
+
clients: [browserAdapter],
|
|
82
|
+
} = await setup()
|
|
83
|
+
|
|
84
|
+
const _browserRepo = new Repo({
|
|
85
|
+
network: [browserAdapter],
|
|
86
|
+
peerId: "browser" as PeerId,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const serverAdapter = new NodeWSServerAdapter(socket)
|
|
90
|
+
const _serverRepo = new Repo({
|
|
91
|
+
network: [serverAdapter],
|
|
92
|
+
peerId: "server" as PeerId,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const disconnectPromise = new Promise<void>(resolve => {
|
|
96
|
+
browserAdapter.on("peer-disconnected", () => resolve())
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
server.close()
|
|
100
|
+
await disconnectPromise
|
|
87
101
|
})
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
102
|
+
|
|
103
|
+
it("should correctly clear event handlers on reconnect", async () => {
|
|
104
|
+
// This reproduces an issue where the BrowserWebSocketClientAdapter.connect
|
|
105
|
+
// call registered event handlers on the websocket but didn't clear them
|
|
106
|
+
// up again on a second call to connect. This combined with the reconnect
|
|
107
|
+
// timer to produce the following sequence of events:
|
|
108
|
+
//
|
|
109
|
+
// - first call to connect creates a socket and registers an "open"
|
|
110
|
+
// handler. The "open" handler will attempt to send a join message
|
|
111
|
+
// - The connection is slow, so the reconnect timer fires before "open",
|
|
112
|
+
// the reconnect timer calls connect again. this.socket is now a new socket
|
|
113
|
+
// - The other end replies to the first socket, "open"
|
|
114
|
+
// - The original "open" handler attempts to send a message, but on the new
|
|
115
|
+
// socket (because it uses `this.socket`), which isn't open yet, so we get an
|
|
116
|
+
// error that the socket is not ready
|
|
117
|
+
const {
|
|
118
|
+
clients: [browser],
|
|
119
|
+
} = await setup()
|
|
120
|
+
|
|
121
|
+
const peerId = "testclient" as PeerId
|
|
122
|
+
browser.connect(peerId)
|
|
123
|
+
|
|
124
|
+
// simulate the reconnect timer firing before the other end has responded
|
|
125
|
+
// (which works here because we haven't yielded to the event loop yet so
|
|
126
|
+
// the server, which is on the same event loop as us, can't respond)
|
|
127
|
+
browser.connect(peerId)
|
|
128
|
+
|
|
129
|
+
// Now yield, so the server responds on the first socket, if the listeners
|
|
130
|
+
// are cleaned up correctly we shouldn't throw
|
|
131
|
+
await pause(1)
|
|
93
132
|
})
|
|
94
133
|
})
|
|
95
134
|
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
135
|
+
describe("NodeWSServerAdapter", () => {
|
|
136
|
+
const serverResponse = async (clientHello: Object) => {
|
|
137
|
+
const { socket, serverUrl } = await setup(0)
|
|
138
|
+
const server = new NodeWSServerAdapter(socket)
|
|
139
|
+
const _serverRepo = new Repo({
|
|
140
|
+
network: [server],
|
|
141
|
+
peerId: "server" as PeerId,
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const clientSocket = new WebSocket(serverUrl)
|
|
145
|
+
await once(clientSocket, "open")
|
|
146
|
+
const serverHelloPromise = once(clientSocket, "message")
|
|
147
|
+
|
|
148
|
+
clientSocket.send(CBOR.encode(clientHello))
|
|
149
|
+
|
|
150
|
+
const serverHello = await serverHelloPromise
|
|
151
|
+
const message = CBOR.decode(serverHello[0] as Uint8Array)
|
|
152
|
+
return message
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
it("should send the negotiated protocol version in its hello message", async () => {
|
|
156
|
+
const response = await serverResponse({
|
|
157
|
+
type: "join",
|
|
158
|
+
senderId: "browser",
|
|
159
|
+
supportedProtocolVersions: ["1"],
|
|
160
|
+
})
|
|
161
|
+
assert.deepEqual(response, {
|
|
162
|
+
type: "peer",
|
|
163
|
+
senderId: "server",
|
|
164
|
+
targetId: "browser",
|
|
165
|
+
selectedProtocolVersion: "1",
|
|
166
|
+
})
|
|
106
167
|
})
|
|
107
|
-
})
|
|
108
|
-
})
|
|
109
168
|
|
|
110
|
-
async
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const serverUrl = `ws://localhost:${port}`
|
|
116
|
-
const adapter = new NodeWSServerAdapter(socket)
|
|
117
|
-
const repo = new Repo({ network: [adapter], peerId: "server" as PeerId })
|
|
118
|
-
|
|
119
|
-
const clientSocket = new WebSocket(serverUrl)
|
|
120
|
-
await once(clientSocket, "open")
|
|
121
|
-
const serverHelloPromise = once(clientSocket, "message")
|
|
122
|
-
|
|
123
|
-
clientSocket.send(CBOR.encode(clientHello))
|
|
124
|
-
|
|
125
|
-
const serverHello = await serverHelloPromise
|
|
126
|
-
const message = CBOR.decode(serverHello[0] as Uint8Array)
|
|
127
|
-
return message
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function firstMessage(
|
|
131
|
-
socket: WebSocket.Server<any>
|
|
132
|
-
): Promise<Object | null> {
|
|
133
|
-
return new Promise((resolve, reject) => {
|
|
134
|
-
socket.once("connection", ws => {
|
|
135
|
-
ws.once("message", (message: any) => {
|
|
136
|
-
resolve(message)
|
|
169
|
+
it("should return an error message if the protocol version is not supported", async () => {
|
|
170
|
+
const response = await serverResponse({
|
|
171
|
+
type: "join",
|
|
172
|
+
senderId: "browser",
|
|
173
|
+
supportedProtocolVersions: ["fake"],
|
|
137
174
|
})
|
|
138
|
-
|
|
139
|
-
|
|
175
|
+
assert.deepEqual(response, {
|
|
176
|
+
type: "error",
|
|
177
|
+
senderId: "server",
|
|
178
|
+
targetId: "browser",
|
|
179
|
+
message: "unsupported protocol version",
|
|
140
180
|
})
|
|
141
181
|
})
|
|
142
|
-
|
|
143
|
-
|
|
182
|
+
|
|
183
|
+
it("should respond with protocol v1 if no protocol version is specified", async () => {
|
|
184
|
+
const response = await serverResponse({
|
|
185
|
+
type: "join",
|
|
186
|
+
senderId: "browser",
|
|
187
|
+
})
|
|
188
|
+
assert.deepEqual(response, {
|
|
189
|
+
type: "peer",
|
|
190
|
+
senderId: "server",
|
|
191
|
+
targetId: "browser",
|
|
192
|
+
selectedProtocolVersion: "1",
|
|
193
|
+
})
|
|
144
194
|
})
|
|
145
195
|
})
|
|
146
|
-
}
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
export const pause = (t = 0) =>
|
|
199
|
+
new Promise<void>(resolve => setTimeout(() => resolve(), t))
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import http from "http"
|
|
2
|
-
import { createWebSocketServer } from "./CreateWebSocketServer.js"
|
|
3
|
-
import WebSocket from "ws"
|
|
4
|
-
|
|
5
|
-
function startServer(port: number) {
|
|
6
|
-
const server = http.createServer()
|
|
7
|
-
const socket = createWebSocketServer(server)
|
|
8
|
-
return new Promise<{
|
|
9
|
-
socket: WebSocket.Server
|
|
10
|
-
server: http.Server
|
|
11
|
-
}>(resolve => {
|
|
12
|
-
server.listen(port, () => resolve({ socket, server }))
|
|
13
|
-
})
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
type WebSocketState =
|
|
17
|
-
| typeof WebSocket.CONNECTING
|
|
18
|
-
| typeof WebSocket.OPEN
|
|
19
|
-
| typeof WebSocket.CLOSING
|
|
20
|
-
| typeof WebSocket.CLOSED
|
|
21
|
-
|
|
22
|
-
function waitForSocketState(socket: WebSocket, state: WebSocketState) {
|
|
23
|
-
return new Promise<void>(function (resolve) {
|
|
24
|
-
setTimeout(function () {
|
|
25
|
-
if (socket.readyState === state) {
|
|
26
|
-
resolve()
|
|
27
|
-
} else {
|
|
28
|
-
waitForSocketState(socket, state).then(resolve)
|
|
29
|
-
}
|
|
30
|
-
}, 5)
|
|
31
|
-
})
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export { startServer, waitForSocketState }
|