@automerge/automerge-repo-network-websocket 0.2.1 → 1.0.0-alpha.2
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 -6
- package/dist/BrowserWebSocketClientAdapter.d.ts.map +1 -1
- package/dist/BrowserWebSocketClientAdapter.js +21 -15
- package/dist/NodeWSServerAdapter.d.ts +2 -2
- package/dist/NodeWSServerAdapter.d.ts.map +1 -1
- package/dist/NodeWSServerAdapter.js +30 -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 +25 -20
- package/src/NodeWSServerAdapter.ts +37 -6
- 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
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
1
|
/// <reference types="ws" />
|
|
3
2
|
import { ChannelId, NetworkAdapter, PeerId } from "@automerge/automerge-repo";
|
|
4
3
|
import WebSocket from "isomorphic-ws";
|
|
@@ -6,15 +5,14 @@ declare abstract class WebSocketNetworkAdapter extends NetworkAdapter {
|
|
|
6
5
|
socket?: WebSocket;
|
|
7
6
|
}
|
|
8
7
|
export declare class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
9
|
-
timerId?:
|
|
8
|
+
timerId?: ReturnType<typeof setTimeout>;
|
|
10
9
|
url: string;
|
|
11
|
-
channels: ChannelId[];
|
|
12
10
|
constructor(url: string);
|
|
13
11
|
connect(peerId: PeerId): void;
|
|
14
|
-
join(
|
|
15
|
-
leave(
|
|
12
|
+
join(): void;
|
|
13
|
+
leave(): void;
|
|
16
14
|
sendMessage(targetId: PeerId, channelId: ChannelId, message: Uint8Array, broadcast: boolean): void;
|
|
17
|
-
announceConnection(
|
|
15
|
+
announceConnection(peerId: PeerId): void;
|
|
18
16
|
receiveMessage(message: Uint8Array): void;
|
|
19
17
|
}
|
|
20
18
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BrowserWebSocketClientAdapter.d.ts","sourceRoot":"","sources":["../src/BrowserWebSocketClientAdapter.ts"],"names":[],"mappings":"
|
|
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;IAGxE,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAA;IACvC,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,14 +2,16 @@ 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.js";
|
|
5
6
|
const log = debug("WebsocketClient");
|
|
6
7
|
class WebSocketNetworkAdapter extends NetworkAdapter {
|
|
7
8
|
socket;
|
|
8
9
|
}
|
|
9
10
|
export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
11
|
+
// Type trickery required for platform independence,
|
|
12
|
+
// see https://stackoverflow.com/questions/45802988/typescript-use-correct-version-of-settimeout-node-vs-window
|
|
10
13
|
timerId;
|
|
11
14
|
url;
|
|
12
|
-
channels = [];
|
|
13
15
|
constructor(url) {
|
|
14
16
|
super();
|
|
15
17
|
this.url = url;
|
|
@@ -25,7 +27,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
25
27
|
log(`@ ${this.url}: open`);
|
|
26
28
|
clearInterval(this.timerId);
|
|
27
29
|
this.timerId = undefined;
|
|
28
|
-
this.
|
|
30
|
+
this.join();
|
|
29
31
|
});
|
|
30
32
|
// When a socket closes, or disconnects, remove it from the array.
|
|
31
33
|
this.socket.addEventListener("close", () => {
|
|
@@ -37,32 +39,27 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
37
39
|
});
|
|
38
40
|
this.socket.addEventListener("message", (event) => this.receiveMessage(event.data));
|
|
39
41
|
}
|
|
40
|
-
join(
|
|
41
|
-
// TODO: the network subsystem should manage this
|
|
42
|
-
if (!this.channels.includes(channelId)) {
|
|
43
|
-
this.channels.push(channelId);
|
|
44
|
-
}
|
|
42
|
+
join() {
|
|
45
43
|
if (!this.socket) {
|
|
46
44
|
throw new Error("WTF, get a socket");
|
|
47
45
|
}
|
|
48
46
|
if (this.socket.readyState === WebSocket.OPEN) {
|
|
49
|
-
this.socket.send(CBOR.encode(
|
|
47
|
+
this.socket.send(CBOR.encode(joinMessage(this.peerId)));
|
|
50
48
|
}
|
|
51
49
|
else {
|
|
52
50
|
this.socket.addEventListener("open", () => {
|
|
53
51
|
if (!this.socket) {
|
|
54
52
|
throw new Error("WTF, get a socket");
|
|
55
53
|
}
|
|
56
|
-
this.socket.send(CBOR.encode(
|
|
54
|
+
this.socket.send(CBOR.encode(joinMessage(this.peerId)));
|
|
57
55
|
}, { once: true });
|
|
58
56
|
}
|
|
59
57
|
}
|
|
60
|
-
leave(
|
|
61
|
-
this.channels = this.channels.filter(c => c !== channelId);
|
|
58
|
+
leave() {
|
|
62
59
|
if (!this.socket) {
|
|
63
60
|
throw new Error("WTF, get a socket");
|
|
64
61
|
}
|
|
65
|
-
this.socket.send(CBOR.encode({ type: "leave",
|
|
62
|
+
this.socket.send(CBOR.encode({ type: "leave", senderId: this.peerId }));
|
|
66
63
|
}
|
|
67
64
|
sendMessage(targetId, channelId, message, broadcast) {
|
|
68
65
|
if (message.byteLength === 0) {
|
|
@@ -88,13 +85,13 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
88
85
|
}
|
|
89
86
|
this.socket.send(arrayBuf);
|
|
90
87
|
}
|
|
91
|
-
announceConnection(
|
|
88
|
+
announceConnection(peerId) {
|
|
92
89
|
// return a peer object
|
|
93
90
|
const myPeerId = this.peerId;
|
|
94
91
|
if (!myPeerId) {
|
|
95
92
|
throw new Error("we should have a peer ID by now");
|
|
96
93
|
}
|
|
97
|
-
this.emit("peer-candidate", { peerId
|
|
94
|
+
this.emit("peer-candidate", { peerId });
|
|
98
95
|
}
|
|
99
96
|
receiveMessage(message) {
|
|
100
97
|
const decoded = CBOR.decode(new Uint8Array(message));
|
|
@@ -109,8 +106,10 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
109
106
|
switch (type) {
|
|
110
107
|
case "peer":
|
|
111
108
|
log(`peer: ${senderId}, ${channelId}`);
|
|
112
|
-
this.announceConnection(
|
|
109
|
+
this.announceConnection(senderId);
|
|
113
110
|
break;
|
|
111
|
+
case "error":
|
|
112
|
+
log(`error: ${decoded.errorMessage}`);
|
|
114
113
|
default:
|
|
115
114
|
this.emit("message", {
|
|
116
115
|
channelId,
|
|
@@ -122,3 +121,10 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
122
121
|
}
|
|
123
122
|
}
|
|
124
123
|
}
|
|
124
|
+
function joinMessage(senderId) {
|
|
125
|
+
return {
|
|
126
|
+
type: "join",
|
|
127
|
+
senderId,
|
|
128
|
+
supportedProtocolVersions: [ProtocolV1],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -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;CA2EtD"}
|
|
@@ -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.js";
|
|
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,26 @@ 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({
|
|
79
|
+
type: "error",
|
|
80
|
+
errorMessage: "unsupported protocol version",
|
|
81
|
+
}));
|
|
82
|
+
this.sockets[senderId].close();
|
|
83
|
+
delete this.sockets[senderId];
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
socket.send(CBOR.encode({
|
|
87
|
+
type: "peer",
|
|
88
|
+
senderId: this.peerId,
|
|
89
|
+
selectedProtocolVersion: ProtocolV1,
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
76
92
|
break;
|
|
77
93
|
case "leave":
|
|
78
94
|
// It doesn't seem like this gets called;
|
|
@@ -99,3 +115,12 @@ export class NodeWSServerAdapter extends NetworkAdapter {
|
|
|
99
115
|
}
|
|
100
116
|
}
|
|
101
117
|
}
|
|
118
|
+
function selectProtocol(versions) {
|
|
119
|
+
if (versions === undefined) {
|
|
120
|
+
return ProtocolV1;
|
|
121
|
+
}
|
|
122
|
+
if (versions.includes(ProtocolV1)) {
|
|
123
|
+
return ProtocolV1;
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type InboundMessagePayload } from "@automerge/automerge-repo";
|
|
2
|
+
import { ProtocolVersion } from "./protocolVersion.js";
|
|
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,EAAE,KAAK,qBAAqB,EAAE,MAAM,2BAA2B,CAAA;AACtE,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAEtD,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.2
|
|
3
|
+
"version": "1.0.0-alpha.2",
|
|
4
4
|
"description": "isomorphic node/browser Websocket network adapter for Automerge Repo",
|
|
5
|
+
"peerDependencies": {
|
|
6
|
+
"@automerge/automerge": "^2.1.0-alpha.10"
|
|
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.2
|
|
20
|
+
"@automerge/automerge-repo": "^1.0.0-alpha.2",
|
|
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": "b5830dde8f135b694809698aaad2a9fdc79a9898"
|
|
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.js"
|
|
12
|
+
import {InboundWebSocketMessage, OutboundWebSocketMessage} from "./message.js"
|
|
11
13
|
const log = debug("WebsocketClient")
|
|
12
14
|
|
|
13
15
|
abstract class WebSocketNetworkAdapter extends NetworkAdapter {
|
|
@@ -15,9 +17,10 @@ abstract class WebSocketNetworkAdapter extends NetworkAdapter {
|
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
18
|
-
|
|
20
|
+
// Type trickery required for platform independence,
|
|
21
|
+
// see https://stackoverflow.com/questions/45802988/typescript-use-correct-version-of-settimeout-node-vs-window
|
|
22
|
+
timerId?: ReturnType<typeof setTimeout>
|
|
19
23
|
url: string
|
|
20
|
-
channels: ChannelId[] = []
|
|
21
24
|
|
|
22
25
|
constructor(url: string) {
|
|
23
26
|
super()
|
|
@@ -37,7 +40,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
37
40
|
log(`@ ${this.url}: open`)
|
|
38
41
|
clearInterval(this.timerId)
|
|
39
42
|
this.timerId = undefined
|
|
40
|
-
this.
|
|
43
|
+
this.join()
|
|
41
44
|
})
|
|
42
45
|
|
|
43
46
|
// When a socket closes, or disconnects, remove it from the array.
|
|
@@ -54,18 +57,13 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
54
57
|
)
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
join(
|
|
58
|
-
// TODO: the network subsystem should manage this
|
|
59
|
-
if (!this.channels.includes(channelId)) {
|
|
60
|
-
this.channels.push(channelId)
|
|
61
|
-
}
|
|
62
|
-
|
|
60
|
+
join() {
|
|
63
61
|
if (!this.socket) {
|
|
64
62
|
throw new Error("WTF, get a socket")
|
|
65
63
|
}
|
|
66
64
|
if (this.socket.readyState === WebSocket.OPEN) {
|
|
67
65
|
this.socket.send(
|
|
68
|
-
CBOR.encode(
|
|
66
|
+
CBOR.encode(joinMessage(this.peerId))
|
|
69
67
|
)
|
|
70
68
|
} else {
|
|
71
69
|
this.socket.addEventListener(
|
|
@@ -75,7 +73,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
75
73
|
throw new Error("WTF, get a socket")
|
|
76
74
|
}
|
|
77
75
|
this.socket.send(
|
|
78
|
-
CBOR.encode(
|
|
76
|
+
CBOR.encode(joinMessage(this.peerId))
|
|
79
77
|
)
|
|
80
78
|
},
|
|
81
79
|
{ once: true }
|
|
@@ -83,14 +81,11 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
83
81
|
}
|
|
84
82
|
}
|
|
85
83
|
|
|
86
|
-
leave(
|
|
87
|
-
this.channels = this.channels.filter(c => c !== channelId)
|
|
84
|
+
leave() {
|
|
88
85
|
if (!this.socket) {
|
|
89
86
|
throw new Error("WTF, get a socket")
|
|
90
87
|
}
|
|
91
|
-
this.socket.send(
|
|
92
|
-
CBOR.encode({ type: "leave", channelId, senderId: this.peerId })
|
|
93
|
-
)
|
|
88
|
+
this.socket.send(CBOR.encode({ type: "leave", senderId: this.peerId }))
|
|
94
89
|
}
|
|
95
90
|
|
|
96
91
|
sendMessage(
|
|
@@ -129,18 +124,18 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
129
124
|
this.socket.send(arrayBuf)
|
|
130
125
|
}
|
|
131
126
|
|
|
132
|
-
announceConnection(
|
|
127
|
+
announceConnection(peerId: PeerId) {
|
|
133
128
|
// return a peer object
|
|
134
129
|
const myPeerId = this.peerId
|
|
135
130
|
if (!myPeerId) {
|
|
136
131
|
throw new Error("we should have a peer ID by now")
|
|
137
132
|
}
|
|
138
133
|
|
|
139
|
-
this.emit("peer-candidate", { peerId
|
|
134
|
+
this.emit("peer-candidate", { peerId })
|
|
140
135
|
}
|
|
141
136
|
|
|
142
137
|
receiveMessage(message: Uint8Array) {
|
|
143
|
-
const decoded:
|
|
138
|
+
const decoded: OutboundWebSocketMessage = CBOR.decode(new Uint8Array(message))
|
|
144
139
|
|
|
145
140
|
const {
|
|
146
141
|
type,
|
|
@@ -163,8 +158,10 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
163
158
|
switch (type) {
|
|
164
159
|
case "peer":
|
|
165
160
|
log(`peer: ${senderId}, ${channelId}`)
|
|
166
|
-
this.announceConnection(
|
|
161
|
+
this.announceConnection(senderId)
|
|
167
162
|
break
|
|
163
|
+
case "error":
|
|
164
|
+
log(`error: ${decoded.errorMessage}`)
|
|
168
165
|
default:
|
|
169
166
|
this.emit("message", {
|
|
170
167
|
channelId,
|
|
@@ -176,3 +173,11 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
176
173
|
}
|
|
177
174
|
}
|
|
178
175
|
}
|
|
176
|
+
|
|
177
|
+
function joinMessage(senderId?: PeerId): Record<string, any> {
|
|
178
|
+
return {
|
|
179
|
+
type: "join",
|
|
180
|
+
senderId,
|
|
181
|
+
supportedProtocolVersions: [ProtocolV1],
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
NetworkAdapter,
|
|
11
11
|
PeerId,
|
|
12
12
|
} from "@automerge/automerge-repo"
|
|
13
|
+
import { ProtocolV1, ProtocolVersion } from "./protocolVersion.js"
|
|
14
|
+
import { InboundWebSocketMessage } from "./message.js"
|
|
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,32 @@ 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
|
-
|
|
122
|
+
const selectedProtocolVersion = selectProtocol(
|
|
123
|
+
supportedProtocolVersions
|
|
121
124
|
)
|
|
125
|
+
if (selectedProtocolVersion === null) {
|
|
126
|
+
socket.send(
|
|
127
|
+
CBOR.encode({
|
|
128
|
+
type: "error",
|
|
129
|
+
errorMessage: "unsupported protocol version",
|
|
130
|
+
})
|
|
131
|
+
)
|
|
132
|
+
this.sockets[senderId].close()
|
|
133
|
+
delete this.sockets[senderId]
|
|
134
|
+
} else {
|
|
135
|
+
socket.send(
|
|
136
|
+
CBOR.encode({
|
|
137
|
+
type: "peer",
|
|
138
|
+
senderId: this.peerId,
|
|
139
|
+
selectedProtocolVersion: ProtocolV1,
|
|
140
|
+
})
|
|
141
|
+
)
|
|
142
|
+
}
|
|
122
143
|
break
|
|
123
144
|
case "leave":
|
|
124
145
|
// It doesn't seem like this gets called;
|
|
@@ -146,3 +167,13 @@ export class NodeWSServerAdapter extends NetworkAdapter {
|
|
|
146
167
|
}
|
|
147
168
|
}
|
|
148
169
|
}
|
|
170
|
+
|
|
171
|
+
function selectProtocol(versions?: ProtocolVersion[]): ProtocolVersion | null {
|
|
172
|
+
if (versions === undefined) {
|
|
173
|
+
return ProtocolV1
|
|
174
|
+
}
|
|
175
|
+
if (versions.includes(ProtocolV1)) {
|
|
176
|
+
return ProtocolV1
|
|
177
|
+
}
|
|
178
|
+
return null
|
|
179
|
+
}
|
package/src/message.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type InboundMessagePayload } from "@automerge/automerge-repo"
|
|
2
|
+
import { ProtocolVersion } from "./protocolVersion.js"
|
|
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
|
-
}
|