@automerge/automerge-repo-network-websocket 0.2.1 → 1.0.0-alpha.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 +3 -4
- package/dist/BrowserWebSocketClientAdapter.d.ts.map +1 -1
- package/dist/BrowserWebSocketClientAdapter.js +19 -15
- package/dist/NodeWSServerAdapter.d.ts +2 -2
- package/dist/NodeWSServerAdapter.d.ts.map +1 -1
- package/dist/NodeWSServerAdapter.js +27 -5
- package/dist/message.d.ts +9 -0
- package/dist/message.d.ts.map +1 -0
- package/dist/message.js +1 -0
- package/dist/protocolVersion.d.ts +3 -0
- package/dist/protocolVersion.d.ts.map +1 -0
- package/dist/protocolVersion.js +1 -0
- package/package.json +8 -4
- package/src/BrowserWebSocketClientAdapter.ts +22 -19
- package/src/NodeWSServerAdapter.ts +31 -7
- package/src/message.ts +10 -0
- package/src/protocolVersion.ts +2 -0
- package/test/Websocket.test.ts +99 -0
- package/dist/WSShared.d.ts +0 -10
- package/dist/WSShared.d.ts.map +0 -1
- package/dist/WSShared.js +0 -86
|
@@ -8,13 +8,12 @@ declare abstract class WebSocketNetworkAdapter extends NetworkAdapter {
|
|
|
8
8
|
export declare class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
9
9
|
timerId?: NodeJS.Timer;
|
|
10
10
|
url: string;
|
|
11
|
-
channels: ChannelId[];
|
|
12
11
|
constructor(url: string);
|
|
13
12
|
connect(peerId: PeerId): void;
|
|
14
|
-
join(
|
|
15
|
-
leave(
|
|
13
|
+
join(): void;
|
|
14
|
+
leave(): void;
|
|
16
15
|
sendMessage(targetId: PeerId, channelId: ChannelId, message: Uint8Array, broadcast: boolean): void;
|
|
17
|
-
announceConnection(
|
|
16
|
+
announceConnection(peerId: PeerId): void;
|
|
18
17
|
receiveMessage(message: Uint8Array): void;
|
|
19
18
|
}
|
|
20
19
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BrowserWebSocketClientAdapter.d.ts","sourceRoot":"","sources":["../src/BrowserWebSocketClientAdapter.ts"],"names":[],"mappings":";;AAAA,OAAO,EACL,SAAS,EAET,cAAc,EACd,MAAM,EACP,MAAM,2BAA2B,CAAA;AAElC,OAAO,SAAS,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"BrowserWebSocketClientAdapter.d.ts","sourceRoot":"","sources":["../src/BrowserWebSocketClientAdapter.ts"],"names":[],"mappings":";;AAAA,OAAO,EACL,SAAS,EAET,cAAc,EACd,MAAM,EACP,MAAM,2BAA2B,CAAA;AAElC,OAAO,SAAS,MAAM,eAAe,CAAA;AAOrC,uBAAe,uBAAwB,SAAQ,cAAc;IAC3D,MAAM,CAAC,EAAE,SAAS,CAAA;CACnB;AAED,qBAAa,6BAA8B,SAAQ,uBAAuB;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC,KAAK,CAAA;IACtB,GAAG,EAAE,MAAM,CAAA;gBAEC,GAAG,EAAE,MAAM;IAKvB,OAAO,CAAC,MAAM,EAAE,MAAM;IA8BtB,IAAI;IAwBJ,KAAK;IAOL,WAAW,CACT,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,UAAU,EACnB,SAAS,EAAE,OAAO;IAgCpB,kBAAkB,CAAC,MAAM,EAAE,MAAM;IAUjC,cAAc,CAAC,OAAO,EAAE,UAAU;CAsCnC"}
|
|
@@ -2,6 +2,7 @@ import { NetworkAdapter, } from "@automerge/automerge-repo";
|
|
|
2
2
|
import * as CBOR from "cbor-x";
|
|
3
3
|
import WebSocket from "isomorphic-ws";
|
|
4
4
|
import debug from "debug";
|
|
5
|
+
import { ProtocolV1 } from "./protocolVersion";
|
|
5
6
|
const log = debug("WebsocketClient");
|
|
6
7
|
class WebSocketNetworkAdapter extends NetworkAdapter {
|
|
7
8
|
socket;
|
|
@@ -9,7 +10,6 @@ class WebSocketNetworkAdapter extends NetworkAdapter {
|
|
|
9
10
|
export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
10
11
|
timerId;
|
|
11
12
|
url;
|
|
12
|
-
channels = [];
|
|
13
13
|
constructor(url) {
|
|
14
14
|
super();
|
|
15
15
|
this.url = url;
|
|
@@ -25,7 +25,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
25
25
|
log(`@ ${this.url}: open`);
|
|
26
26
|
clearInterval(this.timerId);
|
|
27
27
|
this.timerId = undefined;
|
|
28
|
-
this.
|
|
28
|
+
this.join();
|
|
29
29
|
});
|
|
30
30
|
// When a socket closes, or disconnects, remove it from the array.
|
|
31
31
|
this.socket.addEventListener("close", () => {
|
|
@@ -37,32 +37,27 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
37
37
|
});
|
|
38
38
|
this.socket.addEventListener("message", (event) => this.receiveMessage(event.data));
|
|
39
39
|
}
|
|
40
|
-
join(
|
|
41
|
-
// TODO: the network subsystem should manage this
|
|
42
|
-
if (!this.channels.includes(channelId)) {
|
|
43
|
-
this.channels.push(channelId);
|
|
44
|
-
}
|
|
40
|
+
join() {
|
|
45
41
|
if (!this.socket) {
|
|
46
42
|
throw new Error("WTF, get a socket");
|
|
47
43
|
}
|
|
48
44
|
if (this.socket.readyState === WebSocket.OPEN) {
|
|
49
|
-
this.socket.send(CBOR.encode(
|
|
45
|
+
this.socket.send(CBOR.encode(joinMessage(this.peerId)));
|
|
50
46
|
}
|
|
51
47
|
else {
|
|
52
48
|
this.socket.addEventListener("open", () => {
|
|
53
49
|
if (!this.socket) {
|
|
54
50
|
throw new Error("WTF, get a socket");
|
|
55
51
|
}
|
|
56
|
-
this.socket.send(CBOR.encode(
|
|
52
|
+
this.socket.send(CBOR.encode(joinMessage(this.peerId)));
|
|
57
53
|
}, { once: true });
|
|
58
54
|
}
|
|
59
55
|
}
|
|
60
|
-
leave(
|
|
61
|
-
this.channels = this.channels.filter(c => c !== channelId);
|
|
56
|
+
leave() {
|
|
62
57
|
if (!this.socket) {
|
|
63
58
|
throw new Error("WTF, get a socket");
|
|
64
59
|
}
|
|
65
|
-
this.socket.send(CBOR.encode({ type: "leave",
|
|
60
|
+
this.socket.send(CBOR.encode({ type: "leave", senderId: this.peerId }));
|
|
66
61
|
}
|
|
67
62
|
sendMessage(targetId, channelId, message, broadcast) {
|
|
68
63
|
if (message.byteLength === 0) {
|
|
@@ -88,13 +83,13 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
88
83
|
}
|
|
89
84
|
this.socket.send(arrayBuf);
|
|
90
85
|
}
|
|
91
|
-
announceConnection(
|
|
86
|
+
announceConnection(peerId) {
|
|
92
87
|
// return a peer object
|
|
93
88
|
const myPeerId = this.peerId;
|
|
94
89
|
if (!myPeerId) {
|
|
95
90
|
throw new Error("we should have a peer ID by now");
|
|
96
91
|
}
|
|
97
|
-
this.emit("peer-candidate", { peerId
|
|
92
|
+
this.emit("peer-candidate", { peerId });
|
|
98
93
|
}
|
|
99
94
|
receiveMessage(message) {
|
|
100
95
|
const decoded = CBOR.decode(new Uint8Array(message));
|
|
@@ -109,8 +104,10 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
109
104
|
switch (type) {
|
|
110
105
|
case "peer":
|
|
111
106
|
log(`peer: ${senderId}, ${channelId}`);
|
|
112
|
-
this.announceConnection(
|
|
107
|
+
this.announceConnection(senderId);
|
|
113
108
|
break;
|
|
109
|
+
case "error":
|
|
110
|
+
log(`error: ${decoded.errorMessage}`);
|
|
114
111
|
default:
|
|
115
112
|
this.emit("message", {
|
|
116
113
|
channelId,
|
|
@@ -122,3 +119,10 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
122
119
|
}
|
|
123
120
|
}
|
|
124
121
|
}
|
|
122
|
+
function joinMessage(senderId) {
|
|
123
|
+
return {
|
|
124
|
+
type: "join",
|
|
125
|
+
senderId,
|
|
126
|
+
supportedProtocolVersions: [ProtocolV1],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -8,8 +8,8 @@ export declare class NodeWSServerAdapter extends NetworkAdapter {
|
|
|
8
8
|
};
|
|
9
9
|
constructor(server: WebSocketServer);
|
|
10
10
|
connect(peerId: PeerId): void;
|
|
11
|
-
join(
|
|
12
|
-
leave(
|
|
11
|
+
join(): void;
|
|
12
|
+
leave(): void;
|
|
13
13
|
sendMessage(targetId: PeerId, channelId: ChannelId, message: Uint8Array, broadcast: boolean): void;
|
|
14
14
|
receiveMessage(message: Uint8Array, socket: WebSocket): void;
|
|
15
15
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NodeWSServerAdapter.d.ts","sourceRoot":"","sources":["../src/NodeWSServerAdapter.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAA;AAK/D,OAAO,EACL,SAAS,EAET,cAAc,EACd,MAAM,EACP,MAAM,2BAA2B,CAAA;
|
|
1
|
+
{"version":3,"file":"NodeWSServerAdapter.d.ts","sourceRoot":"","sources":["../src/NodeWSServerAdapter.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAA;AAK/D,OAAO,EACL,SAAS,EAET,cAAc,EACd,MAAM,EACP,MAAM,2BAA2B,CAAA;AAIlC,qBAAa,mBAAoB,SAAQ,cAAc;IACrD,MAAM,EAAE,eAAe,CAAA;IACvB,OAAO,EAAE;QAAE,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAK;gBAEjC,MAAM,EAAE,eAAe;IAKnC,OAAO,CAAC,MAAM,EAAE,MAAM;IAmBtB,IAAI;IAIJ,KAAK;IAIL,WAAW,CACT,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,UAAU,EACnB,SAAS,EAAE,OAAO;IAsCpB,cAAc,CAAC,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS;CAoEtD"}
|
|
@@ -2,6 +2,7 @@ import * as CBOR from "cbor-x";
|
|
|
2
2
|
import debug from "debug";
|
|
3
3
|
const log = debug("WebsocketServer");
|
|
4
4
|
import { NetworkAdapter, } from "@automerge/automerge-repo";
|
|
5
|
+
import { ProtocolV1 } from "./protocolVersion";
|
|
5
6
|
export class NodeWSServerAdapter extends NetworkAdapter {
|
|
6
7
|
server;
|
|
7
8
|
sockets = {};
|
|
@@ -24,10 +25,10 @@ export class NodeWSServerAdapter extends NetworkAdapter {
|
|
|
24
25
|
socket.on("message", message => this.receiveMessage(message, socket));
|
|
25
26
|
});
|
|
26
27
|
}
|
|
27
|
-
join(
|
|
28
|
+
join() {
|
|
28
29
|
// throw new Error("The server doesn't join channels.")
|
|
29
30
|
}
|
|
30
|
-
leave(
|
|
31
|
+
leave() {
|
|
31
32
|
// throw new Error("The server doesn't join channels.")
|
|
32
33
|
}
|
|
33
34
|
sendMessage(targetId, channelId, message, broadcast) {
|
|
@@ -59,7 +60,7 @@ export class NodeWSServerAdapter extends NetworkAdapter {
|
|
|
59
60
|
}
|
|
60
61
|
receiveMessage(message, socket) {
|
|
61
62
|
const cbor = CBOR.decode(message);
|
|
62
|
-
const { type, channelId, senderId, targetId, message: data, broadcast, } = cbor;
|
|
63
|
+
const { type, channelId, senderId, targetId, message: data, broadcast, supportedProtocolVersions, } = cbor;
|
|
63
64
|
const myPeerId = this.peerId;
|
|
64
65
|
if (!myPeerId) {
|
|
65
66
|
throw new Error("Missing my peer ID.");
|
|
@@ -68,11 +69,23 @@ export class NodeWSServerAdapter extends NetworkAdapter {
|
|
|
68
69
|
switch (type) {
|
|
69
70
|
case "join":
|
|
70
71
|
// Let the rest of the system know that we have a new connection.
|
|
71
|
-
this.emit("peer-candidate", { peerId: senderId
|
|
72
|
+
this.emit("peer-candidate", { peerId: senderId });
|
|
72
73
|
this.sockets[senderId] = socket;
|
|
73
74
|
// In this client-server connection, there's only ever one peer: us!
|
|
74
75
|
// (and we pretend to be joined to every channel)
|
|
75
|
-
|
|
76
|
+
const selectedProtocolVersion = selectProtocol(supportedProtocolVersions);
|
|
77
|
+
if (selectedProtocolVersion === null) {
|
|
78
|
+
socket.send(CBOR.encode({ type: "error", errorMessage: "unsupported protocol version" }));
|
|
79
|
+
this.sockets[senderId].close();
|
|
80
|
+
delete this.sockets[senderId];
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
socket.send(CBOR.encode({
|
|
84
|
+
type: "peer",
|
|
85
|
+
senderId: this.peerId,
|
|
86
|
+
selectedProtocolVersion: ProtocolV1,
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
76
89
|
break;
|
|
77
90
|
case "leave":
|
|
78
91
|
// It doesn't seem like this gets called;
|
|
@@ -99,3 +112,12 @@ export class NodeWSServerAdapter extends NetworkAdapter {
|
|
|
99
112
|
}
|
|
100
113
|
}
|
|
101
114
|
}
|
|
115
|
+
function selectProtocol(versions) {
|
|
116
|
+
if (versions === undefined) {
|
|
117
|
+
return ProtocolV1;
|
|
118
|
+
}
|
|
119
|
+
if (versions.includes(ProtocolV1)) {
|
|
120
|
+
return ProtocolV1;
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type InboundMessagePayload } from "@automerge/automerge-repo";
|
|
2
|
+
import { ProtocolVersion } from "./protocolVersion";
|
|
3
|
+
export interface InboundWebSocketMessage extends InboundMessagePayload {
|
|
4
|
+
supportedProtocolVersions?: ProtocolVersion[];
|
|
5
|
+
}
|
|
6
|
+
export interface OutboundWebSocketMessage extends InboundMessagePayload {
|
|
7
|
+
errorMessage?: string;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=message.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../src/message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,KAAK,qBAAqB,EAAC,MAAM,2BAA2B,CAAA;AACpE,OAAO,EAAC,eAAe,EAAC,MAAM,mBAAmB,CAAA;AAEjD,MAAM,WAAW,uBAAwB,SAAQ,qBAAqB;IACpE,yBAAyB,CAAC,EAAE,eAAe,EAAE,CAAA;CAC9C;AAED,MAAM,WAAW,wBAAyB,SAAQ,qBAAqB;IACrE,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB"}
|
package/dist/message.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocolVersion.d.ts","sourceRoot":"","sources":["../src/protocolVersion.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU,MAAM,CAAA;AAC7B,MAAM,MAAM,eAAe,GAAG,OAAO,UAAU,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ProtocolV1 = "1";
|
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automerge/automerge-repo-network-websocket",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-alpha.0",
|
|
4
4
|
"description": "isomorphic node/browser Websocket network adapter for Automerge Repo",
|
|
5
|
+
"peerDependencies": {
|
|
6
|
+
"@automerge/automerge": "^2.1.0-alpha.9"
|
|
7
|
+
},
|
|
5
8
|
"repository": "https://github.com/automerge/automerge-repo",
|
|
6
9
|
"author": "Peter van Hardenberg <pvh@pvh.ca>",
|
|
7
10
|
"license": "MIT",
|
|
@@ -14,10 +17,11 @@
|
|
|
14
17
|
"test": "mocha --no-warnings --experimental-specifier-resolution=node --exit"
|
|
15
18
|
},
|
|
16
19
|
"dependencies": {
|
|
17
|
-
"@automerge/automerge-repo": "^0.
|
|
20
|
+
"@automerge/automerge-repo": "^1.0.0-alpha.0",
|
|
18
21
|
"cbor-x": "^1.3.0",
|
|
19
22
|
"eventemitter3": "^4.0.7",
|
|
20
|
-
"isomorphic-ws": "^5.0.0"
|
|
23
|
+
"isomorphic-ws": "^5.0.0",
|
|
24
|
+
"ws": "^8.7.0"
|
|
21
25
|
},
|
|
22
26
|
"watch": {
|
|
23
27
|
"build": {
|
|
@@ -30,5 +34,5 @@
|
|
|
30
34
|
"publishConfig": {
|
|
31
35
|
"access": "public"
|
|
32
36
|
},
|
|
33
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "38c0c32796ddca5f86a2e55ab0f1202a2ce107c8"
|
|
34
38
|
}
|
|
@@ -8,6 +8,8 @@ import * as CBOR from "cbor-x"
|
|
|
8
8
|
import WebSocket from "isomorphic-ws"
|
|
9
9
|
|
|
10
10
|
import debug from "debug"
|
|
11
|
+
import {ProtocolV1} from "./protocolVersion"
|
|
12
|
+
import {InboundWebSocketMessage, OutboundWebSocketMessage} from "./message"
|
|
11
13
|
const log = debug("WebsocketClient")
|
|
12
14
|
|
|
13
15
|
abstract class WebSocketNetworkAdapter extends NetworkAdapter {
|
|
@@ -17,7 +19,6 @@ abstract class WebSocketNetworkAdapter extends NetworkAdapter {
|
|
|
17
19
|
export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
18
20
|
timerId?: NodeJS.Timer
|
|
19
21
|
url: string
|
|
20
|
-
channels: ChannelId[] = []
|
|
21
22
|
|
|
22
23
|
constructor(url: string) {
|
|
23
24
|
super()
|
|
@@ -37,7 +38,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
37
38
|
log(`@ ${this.url}: open`)
|
|
38
39
|
clearInterval(this.timerId)
|
|
39
40
|
this.timerId = undefined
|
|
40
|
-
this.
|
|
41
|
+
this.join()
|
|
41
42
|
})
|
|
42
43
|
|
|
43
44
|
// When a socket closes, or disconnects, remove it from the array.
|
|
@@ -54,18 +55,13 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
54
55
|
)
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
join(
|
|
58
|
-
// TODO: the network subsystem should manage this
|
|
59
|
-
if (!this.channels.includes(channelId)) {
|
|
60
|
-
this.channels.push(channelId)
|
|
61
|
-
}
|
|
62
|
-
|
|
58
|
+
join() {
|
|
63
59
|
if (!this.socket) {
|
|
64
60
|
throw new Error("WTF, get a socket")
|
|
65
61
|
}
|
|
66
62
|
if (this.socket.readyState === WebSocket.OPEN) {
|
|
67
63
|
this.socket.send(
|
|
68
|
-
CBOR.encode(
|
|
64
|
+
CBOR.encode(joinMessage(this.peerId))
|
|
69
65
|
)
|
|
70
66
|
} else {
|
|
71
67
|
this.socket.addEventListener(
|
|
@@ -75,7 +71,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
75
71
|
throw new Error("WTF, get a socket")
|
|
76
72
|
}
|
|
77
73
|
this.socket.send(
|
|
78
|
-
CBOR.encode(
|
|
74
|
+
CBOR.encode(joinMessage(this.peerId))
|
|
79
75
|
)
|
|
80
76
|
},
|
|
81
77
|
{ once: true }
|
|
@@ -83,14 +79,11 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
83
79
|
}
|
|
84
80
|
}
|
|
85
81
|
|
|
86
|
-
leave(
|
|
87
|
-
this.channels = this.channels.filter(c => c !== channelId)
|
|
82
|
+
leave() {
|
|
88
83
|
if (!this.socket) {
|
|
89
84
|
throw new Error("WTF, get a socket")
|
|
90
85
|
}
|
|
91
|
-
this.socket.send(
|
|
92
|
-
CBOR.encode({ type: "leave", channelId, senderId: this.peerId })
|
|
93
|
-
)
|
|
86
|
+
this.socket.send(CBOR.encode({ type: "leave", senderId: this.peerId }))
|
|
94
87
|
}
|
|
95
88
|
|
|
96
89
|
sendMessage(
|
|
@@ -129,18 +122,18 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
129
122
|
this.socket.send(arrayBuf)
|
|
130
123
|
}
|
|
131
124
|
|
|
132
|
-
announceConnection(
|
|
125
|
+
announceConnection(peerId: PeerId) {
|
|
133
126
|
// return a peer object
|
|
134
127
|
const myPeerId = this.peerId
|
|
135
128
|
if (!myPeerId) {
|
|
136
129
|
throw new Error("we should have a peer ID by now")
|
|
137
130
|
}
|
|
138
131
|
|
|
139
|
-
this.emit("peer-candidate", { peerId
|
|
132
|
+
this.emit("peer-candidate", { peerId })
|
|
140
133
|
}
|
|
141
134
|
|
|
142
135
|
receiveMessage(message: Uint8Array) {
|
|
143
|
-
const decoded:
|
|
136
|
+
const decoded: OutboundWebSocketMessage = CBOR.decode(new Uint8Array(message))
|
|
144
137
|
|
|
145
138
|
const {
|
|
146
139
|
type,
|
|
@@ -163,8 +156,10 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
163
156
|
switch (type) {
|
|
164
157
|
case "peer":
|
|
165
158
|
log(`peer: ${senderId}, ${channelId}`)
|
|
166
|
-
this.announceConnection(
|
|
159
|
+
this.announceConnection(senderId)
|
|
167
160
|
break
|
|
161
|
+
case "error":
|
|
162
|
+
log(`error: ${decoded.errorMessage}`)
|
|
168
163
|
default:
|
|
169
164
|
this.emit("message", {
|
|
170
165
|
channelId,
|
|
@@ -176,3 +171,11 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
176
171
|
}
|
|
177
172
|
}
|
|
178
173
|
}
|
|
174
|
+
|
|
175
|
+
function joinMessage(senderId?: PeerId): Record<string, any> {
|
|
176
|
+
return {
|
|
177
|
+
type: "join",
|
|
178
|
+
senderId,
|
|
179
|
+
supportedProtocolVersions: [ProtocolV1],
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
NetworkAdapter,
|
|
11
11
|
PeerId,
|
|
12
12
|
} from "@automerge/automerge-repo"
|
|
13
|
+
import {ProtocolV1, ProtocolVersion} from "./protocolVersion"
|
|
14
|
+
import {InboundWebSocketMessage} from "./message"
|
|
13
15
|
|
|
14
16
|
export class NodeWSServerAdapter extends NetworkAdapter {
|
|
15
17
|
server: WebSocketServer
|
|
@@ -39,11 +41,11 @@ export class NodeWSServerAdapter extends NetworkAdapter {
|
|
|
39
41
|
})
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
join(
|
|
44
|
+
join() {
|
|
43
45
|
// throw new Error("The server doesn't join channels.")
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
leave(
|
|
48
|
+
leave() {
|
|
47
49
|
// throw new Error("The server doesn't join channels.")
|
|
48
50
|
}
|
|
49
51
|
|
|
@@ -90,7 +92,7 @@ export class NodeWSServerAdapter extends NetworkAdapter {
|
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
receiveMessage(message: Uint8Array, socket: WebSocket) {
|
|
93
|
-
const cbor:
|
|
95
|
+
const cbor: InboundWebSocketMessage = CBOR.decode(message)
|
|
94
96
|
|
|
95
97
|
const {
|
|
96
98
|
type,
|
|
@@ -99,6 +101,7 @@ export class NodeWSServerAdapter extends NetworkAdapter {
|
|
|
99
101
|
targetId,
|
|
100
102
|
message: data,
|
|
101
103
|
broadcast,
|
|
104
|
+
supportedProtocolVersions,
|
|
102
105
|
} = cbor
|
|
103
106
|
|
|
104
107
|
const myPeerId = this.peerId
|
|
@@ -111,14 +114,25 @@ export class NodeWSServerAdapter extends NetworkAdapter {
|
|
|
111
114
|
switch (type) {
|
|
112
115
|
case "join":
|
|
113
116
|
// Let the rest of the system know that we have a new connection.
|
|
114
|
-
this.emit("peer-candidate", { peerId: senderId
|
|
117
|
+
this.emit("peer-candidate", { peerId: senderId })
|
|
115
118
|
this.sockets[senderId] = socket
|
|
116
119
|
|
|
117
120
|
// In this client-server connection, there's only ever one peer: us!
|
|
118
121
|
// (and we pretend to be joined to every channel)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
const selectedProtocolVersion = selectProtocol(supportedProtocolVersions)
|
|
123
|
+
if (selectedProtocolVersion === null) {
|
|
124
|
+
socket.send(CBOR.encode({ type: "error", errorMessage: "unsupported protocol version"}))
|
|
125
|
+
this.sockets[senderId].close()
|
|
126
|
+
delete this.sockets[senderId]
|
|
127
|
+
} else {
|
|
128
|
+
socket.send(
|
|
129
|
+
CBOR.encode({
|
|
130
|
+
type: "peer",
|
|
131
|
+
senderId: this.peerId,
|
|
132
|
+
selectedProtocolVersion: ProtocolV1,
|
|
133
|
+
})
|
|
134
|
+
)
|
|
135
|
+
}
|
|
122
136
|
break
|
|
123
137
|
case "leave":
|
|
124
138
|
// It doesn't seem like this gets called;
|
|
@@ -146,3 +160,13 @@ export class NodeWSServerAdapter extends NetworkAdapter {
|
|
|
146
160
|
}
|
|
147
161
|
}
|
|
148
162
|
}
|
|
163
|
+
|
|
164
|
+
function selectProtocol(versions?: ProtocolVersion[]): ProtocolVersion | null {
|
|
165
|
+
if (versions === undefined) {
|
|
166
|
+
return ProtocolV1
|
|
167
|
+
}
|
|
168
|
+
if (versions.includes(ProtocolV1)) {
|
|
169
|
+
return ProtocolV1
|
|
170
|
+
}
|
|
171
|
+
return null
|
|
172
|
+
}
|
package/src/message.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {type InboundMessagePayload} from "@automerge/automerge-repo"
|
|
2
|
+
import {ProtocolVersion} from "./protocolVersion"
|
|
3
|
+
|
|
4
|
+
export interface InboundWebSocketMessage extends InboundMessagePayload {
|
|
5
|
+
supportedProtocolVersions?: ProtocolVersion[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface OutboundWebSocketMessage extends InboundMessagePayload {
|
|
9
|
+
errorMessage?: string
|
|
10
|
+
}
|
package/test/Websocket.test.ts
CHANGED
|
@@ -2,6 +2,11 @@ import { runAdapterTests } from "../../automerge-repo/src/helpers/tests/network-
|
|
|
2
2
|
import { BrowserWebSocketClientAdapter } from "../src/BrowserWebSocketClientAdapter"
|
|
3
3
|
import { NodeWSServerAdapter } from "../src/NodeWSServerAdapter"
|
|
4
4
|
import { startServer } from "./utilities/WebSockets"
|
|
5
|
+
import * as CBOR from "cbor-x"
|
|
6
|
+
import WebSocket, {AddressInfo} from "ws"
|
|
7
|
+
import { assert } from "chai"
|
|
8
|
+
import {ChannelId, PeerId, Repo} from "@automerge/automerge-repo"
|
|
9
|
+
import { once } from "events"
|
|
5
10
|
|
|
6
11
|
describe("Websocket adapters", async () => {
|
|
7
12
|
let port = 8080
|
|
@@ -23,3 +28,97 @@ describe("Websocket adapters", async () => {
|
|
|
23
28
|
return { adapters: [serverAdapter, aliceAdapter, bobAdapter], teardown }
|
|
24
29
|
})
|
|
25
30
|
})
|
|
31
|
+
|
|
32
|
+
describe("The BrowserWebSocketClientAdapter", () => {
|
|
33
|
+
it("should advertise the protocol versions it supports in its join message", async () => {
|
|
34
|
+
const { socket, server } = await startServer(0)
|
|
35
|
+
let port = (server.address()!! as AddressInfo).port
|
|
36
|
+
const serverUrl = `ws://localhost:${port}`
|
|
37
|
+
const helloPromise = firstMessage(socket)
|
|
38
|
+
|
|
39
|
+
const client = new BrowserWebSocketClientAdapter(serverUrl)
|
|
40
|
+
const repo = new Repo({network: [client], peerId: "browser" as PeerId})
|
|
41
|
+
|
|
42
|
+
const hello = await helloPromise
|
|
43
|
+
|
|
44
|
+
const message = CBOR.decode(hello as Uint8Array)
|
|
45
|
+
assert.deepEqual(message, {
|
|
46
|
+
type: "join",
|
|
47
|
+
senderId: "browser",
|
|
48
|
+
supportedProtocolVersions: ["1"]
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe("The NodeWSServerAdapter", () => {
|
|
54
|
+
it("should send the negotiated protocol version in its hello message", async () => {
|
|
55
|
+
const response = await serverHelloGivenClientHello({
|
|
56
|
+
type: "join",
|
|
57
|
+
senderId: "browser",
|
|
58
|
+
supportedProtocolVersions: ["1"]
|
|
59
|
+
})
|
|
60
|
+
assert.deepEqual<any>(response, {
|
|
61
|
+
type: "peer",
|
|
62
|
+
senderId: "server",
|
|
63
|
+
selectedProtocolVersion: "1"
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it("should return an error message if the protocol version is not supported", async () => {
|
|
68
|
+
const response = await serverHelloGivenClientHello({
|
|
69
|
+
type: "join",
|
|
70
|
+
senderId: "browser",
|
|
71
|
+
supportedProtocolVersions: ["fake"]
|
|
72
|
+
})
|
|
73
|
+
assert.deepEqual<any>(response, {
|
|
74
|
+
type: "error",
|
|
75
|
+
errorMessage: "unsupported protocol version",
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it("should respond with protocol v1 if no protocol version is specified", async () => {
|
|
80
|
+
const response = await serverHelloGivenClientHello({
|
|
81
|
+
type: "join",
|
|
82
|
+
senderId: "browser",
|
|
83
|
+
})
|
|
84
|
+
assert.deepEqual<any>(response, {
|
|
85
|
+
type: "peer",
|
|
86
|
+
senderId: "server",
|
|
87
|
+
selectedProtocolVersion: "1"
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
async function serverHelloGivenClientHello(clientHello: Object): Promise<Object | null> {
|
|
93
|
+
const { socket, server } = await startServer(0)
|
|
94
|
+
let port = (server.address()!! as AddressInfo).port
|
|
95
|
+
const serverUrl = `ws://localhost:${port}`
|
|
96
|
+
const adapter = new NodeWSServerAdapter(socket)
|
|
97
|
+
const repo = new Repo({network: [adapter], peerId: "server" as PeerId})
|
|
98
|
+
|
|
99
|
+
const clientSocket = new WebSocket(serverUrl)
|
|
100
|
+
await once(clientSocket, "open")
|
|
101
|
+
const serverHelloPromise = once(clientSocket, "message")
|
|
102
|
+
|
|
103
|
+
clientSocket.send(CBOR.encode(clientHello))
|
|
104
|
+
|
|
105
|
+
const serverHello = await serverHelloPromise
|
|
106
|
+
const message = CBOR.decode(serverHello[0] as Uint8Array)
|
|
107
|
+
return message
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function firstMessage(socket: WebSocket.Server<any>): Promise<Object | null> {
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
socket.once("connection", (ws) => {
|
|
113
|
+
ws.once("message", (message: any) => {
|
|
114
|
+
resolve(message)
|
|
115
|
+
})
|
|
116
|
+
ws.once("error", (error: any) => {
|
|
117
|
+
reject(error)
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
socket.once("error", (error) => {
|
|
121
|
+
reject(error)
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
}
|
package/dist/WSShared.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/// <reference types="ws" />
|
|
2
|
-
import { ChannelId, NetworkAdapter, PeerId } from "automerge-repo";
|
|
3
|
-
import WebSocket from "isomorphic-ws";
|
|
4
|
-
export interface WebSocketNetworkAdapter extends NetworkAdapter {
|
|
5
|
-
client?: WebSocket;
|
|
6
|
-
}
|
|
7
|
-
export declare function sendMessage(destinationId: PeerId, socket: WebSocket, channelId: ChannelId, senderId: PeerId, message: Uint8Array): void;
|
|
8
|
-
export declare function receiveMessageClient(message: Uint8Array, self: WebSocketNetworkAdapter): void;
|
|
9
|
-
export declare function receiveMessageServer(message: Uint8Array, socket: WebSocket, self: WebSocketNetworkAdapter): void;
|
|
10
|
-
//# sourceMappingURL=WSShared.d.ts.map
|
package/dist/WSShared.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"WSShared.d.ts","sourceRoot":"","sources":["../src/WSShared.ts"],"names":[],"mappings":";AACA,OAAO,EACL,SAAS,EAET,cAAc,EAEd,MAAM,EACP,MAAM,gBAAgB,CAAA;AAEvB,OAAO,SAAS,MAAM,eAAe,CAAA;AAErC,MAAM,WAAW,uBAAwB,SAAQ,cAAc;IAC7D,MAAM,CAAC,EAAE,SAAS,CAAA;CACnB;AAED,wBAAgB,WAAW,CACzB,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,SAAS,EACjB,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,UAAU,QAwBpB;AAiBD,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,UAAU,EACnB,IAAI,EAAE,uBAAuB,QAqC9B;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,UAAU,EACnB,MAAM,EAAE,SAAS,EACjB,IAAI,EAAE,uBAAuB,QAwC9B"}
|
package/dist/WSShared.js
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import * as CBOR from "cbor-x";
|
|
2
|
-
import WebSocket from "isomorphic-ws";
|
|
3
|
-
export function sendMessage(destinationId, socket, channelId, senderId, message) {
|
|
4
|
-
if (message.byteLength === 0) {
|
|
5
|
-
throw new Error("tried to send a zero-length message");
|
|
6
|
-
}
|
|
7
|
-
const decoded = {
|
|
8
|
-
senderId,
|
|
9
|
-
channelId,
|
|
10
|
-
type: "sync",
|
|
11
|
-
data: message,
|
|
12
|
-
};
|
|
13
|
-
const encoded = CBOR.encode(decoded);
|
|
14
|
-
// This incantation deals with websocket sending the whole
|
|
15
|
-
// underlying buffer even if we just have a uint8array view on it
|
|
16
|
-
const arrayBuf = encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength);
|
|
17
|
-
console.log(`[${senderId}->${destinationId}@${channelId}] "sync" | ${arrayBuf.byteLength} bytes`);
|
|
18
|
-
socket.send(arrayBuf);
|
|
19
|
-
}
|
|
20
|
-
function prepareConnection(channelId, destinationId, socket, sourceId) {
|
|
21
|
-
const connection = {
|
|
22
|
-
close: () => socket.close(),
|
|
23
|
-
isOpen: () => socket.readyState === WebSocket.OPEN,
|
|
24
|
-
send: (message) => sendMessage(destinationId, socket, channelId, sourceId, message),
|
|
25
|
-
};
|
|
26
|
-
return connection;
|
|
27
|
-
}
|
|
28
|
-
export function receiveMessageClient(message, self) {
|
|
29
|
-
const decoded = CBOR.decode(new Uint8Array(message));
|
|
30
|
-
const { type, senderId, channelId, data } = decoded;
|
|
31
|
-
const socket = self.client;
|
|
32
|
-
if (!socket) {
|
|
33
|
-
throw new Error("Missing client at receiveMessage");
|
|
34
|
-
}
|
|
35
|
-
if (message.byteLength === 0) {
|
|
36
|
-
throw new Error("received a zero-length message");
|
|
37
|
-
}
|
|
38
|
-
switch (type) {
|
|
39
|
-
case "peer":
|
|
40
|
-
// console.log(`peer: ${senderId}, ${channelId}`)
|
|
41
|
-
const myPeerId = self.peerId;
|
|
42
|
-
if (!myPeerId) {
|
|
43
|
-
throw new Error("Local peer ID not set!");
|
|
44
|
-
}
|
|
45
|
-
const connection = prepareConnection(channelId, senderId, socket, myPeerId);
|
|
46
|
-
self.emit("peer-candidate", { peerId: senderId, channelId, connection });
|
|
47
|
-
break;
|
|
48
|
-
default:
|
|
49
|
-
self.emit("message", {
|
|
50
|
-
channelId,
|
|
51
|
-
senderId,
|
|
52
|
-
message: new Uint8Array(data),
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
export function receiveMessageServer(message, socket, self) {
|
|
57
|
-
const cbor = CBOR.decode(message);
|
|
58
|
-
const { type, channelId, senderId, data } = cbor;
|
|
59
|
-
const myPeerId = self.peerId;
|
|
60
|
-
if (!myPeerId) {
|
|
61
|
-
throw new Error("Missing my peer ID.");
|
|
62
|
-
}
|
|
63
|
-
console.log(`[${senderId}->${myPeerId}@${channelId}] ${type} | ${message.byteLength} bytes`);
|
|
64
|
-
switch (type) {
|
|
65
|
-
case "join":
|
|
66
|
-
// Let the rest of the system know that we have a new connection.
|
|
67
|
-
const connection = prepareConnection(channelId, senderId, socket, myPeerId);
|
|
68
|
-
self.emit("peer-candidate", { peerId: senderId, channelId, connection });
|
|
69
|
-
// In this client-server connection, there's only ever one peer: us!
|
|
70
|
-
socket.send(CBOR.encode({ type: "peer", senderId: self.peerId, channelId }));
|
|
71
|
-
break;
|
|
72
|
-
case "leave":
|
|
73
|
-
// ?
|
|
74
|
-
break;
|
|
75
|
-
case "sync":
|
|
76
|
-
self.emit("message", {
|
|
77
|
-
senderId,
|
|
78
|
-
channelId,
|
|
79
|
-
message: new Uint8Array(data),
|
|
80
|
-
});
|
|
81
|
-
break;
|
|
82
|
-
default:
|
|
83
|
-
// console.log("unrecognized message type")
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
}
|