@automerge/automerge-repo-network-websocket 0.2.0 → 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.
@@ -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(channelId: ChannelId): void;
15
- leave(channelId: ChannelId): void;
13
+ join(): void;
14
+ leave(): void;
16
15
  sendMessage(targetId: PeerId, channelId: ChannelId, message: Uint8Array, broadcast: boolean): void;
17
- announceConnection(channelId: ChannelId, peerId: PeerId): void;
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;AAKrC,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;IACX,QAAQ,EAAE,SAAS,EAAE,CAAK;gBAEd,GAAG,EAAE,MAAM;IAKvB,OAAO,CAAC,MAAM,EAAE,MAAM;IA8BtB,IAAI,CAAC,SAAS,EAAE,SAAS;IA6BzB,KAAK,CAAC,SAAS,EAAE,SAAS;IAU1B,WAAW,CACT,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,UAAU,EACnB,SAAS,EAAE,OAAO;IAgCpB,kBAAkB,CAAC,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM;IAUvD,cAAc,CAAC,OAAO,EAAE,UAAU;CAoCnC"}
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.channels.forEach(c => this.join(c));
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(channelId) {
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({ type: "join", channelId, senderId: this.peerId }));
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({ type: "join", channelId, senderId: this.peerId }));
52
+ this.socket.send(CBOR.encode(joinMessage(this.peerId)));
57
53
  }, { once: true });
58
54
  }
59
55
  }
60
- leave(channelId) {
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", channelId, senderId: this.peerId }));
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(channelId, peerId) {
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, channelId });
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(channelId, senderId);
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(docId: ChannelId): void;
12
- leave(docId: ChannelId): void;
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;AAElC,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,CAAC,KAAK,EAAE,SAAS;IAIrB,KAAK,CAAC,KAAK,EAAE,SAAS;IAItB,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;CAwDtD"}
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(docId) {
28
+ join() {
28
29
  // throw new Error("The server doesn't join channels.")
29
30
  }
30
- leave(docId) {
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, channelId });
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
- socket.send(CBOR.encode({ type: "peer", senderId: this.peerId, channelId }));
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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export declare const ProtocolV1 = "1";
2
+ export type ProtocolVersion = typeof ProtocolV1;
3
+ //# sourceMappingURL=protocolVersion.d.ts.map
@@ -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.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.2.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": "b17ea68dce605c06e57e4fcd6e6295ef968a8b6d"
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.channels.forEach(c => this.join(c))
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(channelId: ChannelId) {
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({ type: "join", channelId, senderId: this.peerId })
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({ type: "join", channelId, senderId: this.peerId })
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(channelId: ChannelId) {
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(channelId: ChannelId, peerId: PeerId) {
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, channelId })
132
+ this.emit("peer-candidate", { peerId })
140
133
  }
141
134
 
142
135
  receiveMessage(message: Uint8Array) {
143
- const decoded: InboundMessagePayload = CBOR.decode(new Uint8Array(message))
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(channelId, senderId)
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(docId: ChannelId) {
44
+ join() {
43
45
  // throw new Error("The server doesn't join channels.")
44
46
  }
45
47
 
46
- leave(docId: ChannelId) {
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: InboundMessagePayload = CBOR.decode(message)
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, channelId })
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
- socket.send(
120
- CBOR.encode({ type: "peer", senderId: this.peerId, channelId })
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
+ }
@@ -0,0 +1,2 @@
1
+ export const ProtocolV1 = "1"
2
+ export type ProtocolVersion = typeof ProtocolV1
@@ -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
+ }
@@ -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
@@ -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
- }