@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.
@@ -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?: NodeJS.Timer;
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(channelId: ChannelId): void;
15
- leave(channelId: ChannelId): void;
12
+ join(): void;
13
+ leave(): void;
16
14
  sendMessage(targetId: PeerId, channelId: ChannelId, message: Uint8Array, broadcast: boolean): void;
17
- announceConnection(channelId: ChannelId, peerId: PeerId): void;
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":";;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;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.channels.forEach(c => this.join(c));
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(channelId) {
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({ type: "join", channelId, senderId: this.peerId }));
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({ type: "join", channelId, senderId: this.peerId }));
54
+ this.socket.send(CBOR.encode(joinMessage(this.peerId)));
57
55
  }, { once: true });
58
56
  }
59
57
  }
60
- leave(channelId) {
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", channelId, senderId: this.peerId }));
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(channelId, peerId) {
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, channelId });
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(channelId, senderId);
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(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;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(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,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, 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({
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"}
@@ -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.1",
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.1",
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": "7f048ecaa62eb1246f54773c6b10bada0767497b"
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
- timerId?: NodeJS.Timer
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.channels.forEach(c => this.join(c))
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(channelId: ChannelId) {
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({ type: "join", channelId, senderId: this.peerId })
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({ type: "join", channelId, senderId: this.peerId })
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(channelId: ChannelId) {
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(channelId: ChannelId, peerId: PeerId) {
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, channelId })
134
+ this.emit("peer-candidate", { peerId })
140
135
  }
141
136
 
142
137
  receiveMessage(message: Uint8Array) {
143
- const decoded: InboundMessagePayload = CBOR.decode(new Uint8Array(message))
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(channelId, senderId)
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(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,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, 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 })
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
+ }
@@ -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
- }