@automerge/automerge-repo-network-websocket 1.0.0-alpha.2 → 1.0.0-alpha.4

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,17 +1,19 @@
1
1
  /// <reference types="ws" />
2
- import { ChannelId, NetworkAdapter, PeerId } from "@automerge/automerge-repo";
2
+ import { NetworkAdapter, PeerId } from "@automerge/automerge-repo";
3
3
  import WebSocket from "isomorphic-ws";
4
+ import { FromClientMessage } from "./messages.js";
4
5
  declare abstract class WebSocketNetworkAdapter extends NetworkAdapter {
5
6
  socket?: WebSocket;
6
7
  }
7
8
  export declare class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
9
+ #private;
8
10
  timerId?: ReturnType<typeof setTimeout>;
9
11
  url: string;
10
12
  constructor(url: string);
11
13
  connect(peerId: PeerId): void;
12
14
  join(): void;
13
15
  leave(): void;
14
- sendMessage(targetId: PeerId, channelId: ChannelId, message: Uint8Array, broadcast: boolean): void;
16
+ send(message: FromClientMessage): void;
15
17
  announceConnection(peerId: PeerId): void;
16
18
  receiveMessage(message: Uint8Array): void;
17
19
  }
@@ -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;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"}
1
+ {"version":3,"file":"BrowserWebSocketClientAdapter.d.ts","sourceRoot":"","sources":["../src/BrowserWebSocketClientAdapter.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,EAAQ,MAAM,2BAA2B,CAAA;AACxE,OAAO,SAAS,MAAM,eAAe,CAAA;AAIrC,OAAO,EACL,iBAAiB,EAGlB,MAAM,eAAe,CAAA;AAMtB,uBAAe,uBAAwB,SAAQ,cAAc;IAC3D,MAAM,CAAC,EAAE,SAAS,CAAA;CACnB;AAED,qBAAa,6BAA8B,SAAQ,uBAAuB;;IAGxE,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAA;IAGvC,GAAG,EAAE,MAAM,CAAA;gBAEC,GAAG,EAAE,MAAM;IAKvB,OAAO,CAAC,MAAM,EAAE,MAAM;IAwCtB,IAAI;IAoBJ,KAAK;IAOL,IAAI,CAAC,OAAO,EAAE,iBAAiB;IAwB/B,kBAAkB,CAAC,MAAM,EAAE,MAAM;IAajC,cAAc,CAAC,OAAO,EAAE,UAAU;CA6BnC"}
@@ -1,16 +1,17 @@
1
- import { NetworkAdapter, } from "@automerge/automerge-repo";
2
- import * as CBOR from "cbor-x";
1
+ import { NetworkAdapter, cbor } from "@automerge/automerge-repo";
3
2
  import WebSocket from "isomorphic-ws";
4
3
  import debug from "debug";
5
4
  import { ProtocolV1 } from "./protocolVersion.js";
5
+ import { isValidMessage } from "@automerge/automerge-repo/dist/network/messages.js";
6
6
  const log = debug("WebsocketClient");
7
7
  class WebSocketNetworkAdapter extends NetworkAdapter {
8
8
  socket;
9
9
  }
10
10
  export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
11
- // Type trickery required for platform independence,
11
+ // Type trickery required for platform independence,
12
12
  // see https://stackoverflow.com/questions/45802988/typescript-use-correct-version-of-settimeout-node-vs-window
13
13
  timerId;
14
+ #startupComplete = false;
14
15
  url;
15
16
  constructor(url) {
16
17
  super();
@@ -38,20 +39,29 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
38
39
  // log("Disconnected from server")
39
40
  });
40
41
  this.socket.addEventListener("message", (event) => this.receiveMessage(event.data));
42
+ // mark this adapter as ready if we haven't received an ack in 1 second.
43
+ // We might hear back from the other end at some point but we shouldn't
44
+ // hold up marking things as unavailable for any longer
45
+ setTimeout(() => {
46
+ if (!this.#startupComplete) {
47
+ this.#startupComplete = true;
48
+ this.emit("ready", { network: this });
49
+ }
50
+ }, 1000);
41
51
  }
42
52
  join() {
43
53
  if (!this.socket) {
44
54
  throw new Error("WTF, get a socket");
45
55
  }
46
56
  if (this.socket.readyState === WebSocket.OPEN) {
47
- this.socket.send(CBOR.encode(joinMessage(this.peerId)));
57
+ this.send(joinMessage(this.peerId));
48
58
  }
49
59
  else {
50
60
  this.socket.addEventListener("open", () => {
51
61
  if (!this.socket) {
52
62
  throw new Error("WTF, get a socket");
53
63
  }
54
- this.socket.send(CBOR.encode(joinMessage(this.peerId)));
64
+ this.send(joinMessage(this.peerId));
55
65
  }, { once: true });
56
66
  }
57
67
  }
@@ -59,31 +69,23 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
59
69
  if (!this.socket) {
60
70
  throw new Error("WTF, get a socket");
61
71
  }
62
- this.socket.send(CBOR.encode({ type: "leave", senderId: this.peerId }));
72
+ this.send({ type: "leave", senderId: this.peerId });
63
73
  }
64
- sendMessage(targetId, channelId, message, broadcast) {
65
- if (message.byteLength === 0) {
74
+ send(message) {
75
+ if ("data" in message && message.data.byteLength === 0) {
66
76
  throw new Error("tried to send a zero-length message");
67
77
  }
68
78
  if (!this.peerId) {
69
79
  throw new Error("Why don't we have a PeerID?");
70
80
  }
71
- const decoded = {
72
- senderId: this.peerId,
73
- targetId,
74
- channelId,
75
- type: "sync",
76
- message,
77
- broadcast,
78
- };
79
- const encoded = CBOR.encode(decoded);
80
- // This incantation deals with websocket sending the whole
81
- // underlying buffer even if we just have a uint8array view on it
82
- const arrayBuf = encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength);
83
81
  if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
84
82
  throw new Error("Websocket Socket not ready!");
85
83
  }
86
- this.socket.send(arrayBuf);
84
+ const encoded = cbor.encode(message);
85
+ // This incantation deals with websocket sending the whole
86
+ // underlying buffer even if we just have a uint8array view on it
87
+ const arrayBuf = encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength);
88
+ this.socket?.send(arrayBuf);
87
89
  }
88
90
  announceConnection(peerId) {
89
91
  // return a peer object
@@ -91,11 +93,15 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
91
93
  if (!myPeerId) {
92
94
  throw new Error("we should have a peer ID by now");
93
95
  }
96
+ if (!this.#startupComplete) {
97
+ this.#startupComplete = true;
98
+ this.emit("ready", { network: this });
99
+ }
94
100
  this.emit("peer-candidate", { peerId });
95
101
  }
96
102
  receiveMessage(message) {
97
- const decoded = CBOR.decode(new Uint8Array(message));
98
- const { type, senderId, targetId, channelId, message: messageData, broadcast, } = decoded;
103
+ const decoded = cbor.decode(new Uint8Array(message));
104
+ const { type, senderId } = decoded;
99
105
  const socket = this.socket;
100
106
  if (!socket) {
101
107
  throw new Error("Missing socket at receiveMessage");
@@ -105,19 +111,17 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
105
111
  }
106
112
  switch (type) {
107
113
  case "peer":
108
- log(`peer: ${senderId}, ${channelId}`);
114
+ log(`peer: ${senderId}`);
109
115
  this.announceConnection(senderId);
110
116
  break;
111
117
  case "error":
112
- log(`error: ${decoded.errorMessage}`);
118
+ log(`error: ${decoded.message}`);
119
+ break;
113
120
  default:
114
- this.emit("message", {
115
- channelId,
116
- senderId,
117
- targetId,
118
- message: new Uint8Array(messageData),
119
- broadcast,
120
- });
121
+ if (!isValidMessage(decoded)) {
122
+ throw new Error("Invalid message received");
123
+ }
124
+ this.emit("message", decoded);
121
125
  }
122
126
  }
123
127
  }
@@ -1,6 +1,7 @@
1
1
  /// <reference types="ws" />
2
2
  import { WebSocket, type WebSocketServer } from "isomorphic-ws";
3
- import { ChannelId, NetworkAdapter, PeerId } from "@automerge/automerge-repo";
3
+ import { NetworkAdapter, type PeerId } from "@automerge/automerge-repo";
4
+ import { FromServerMessage } from "./messages.js";
4
5
  export declare class NodeWSServerAdapter extends NetworkAdapter {
5
6
  server: WebSocketServer;
6
7
  sockets: {
@@ -10,7 +11,7 @@ export declare class NodeWSServerAdapter extends NetworkAdapter {
10
11
  connect(peerId: PeerId): void;
11
12
  join(): void;
12
13
  leave(): void;
13
- sendMessage(targetId: PeerId, channelId: ChannelId, message: Uint8Array, broadcast: boolean): void;
14
+ send(message: FromServerMessage): void;
14
15
  receiveMessage(message: Uint8Array, socket: WebSocket): void;
15
16
  }
16
17
  //# sourceMappingURL=NodeWSServerAdapter.d.ts.map
@@ -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;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"}
1
+ {"version":3,"file":"NodeWSServerAdapter.d.ts","sourceRoot":"","sources":["../src/NodeWSServerAdapter.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAA;AAM/D,OAAO,EACL,cAAc,EAGd,KAAK,MAAM,EACZ,MAAM,2BAA2B,CAAA;AAClC,OAAO,EAAqB,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAIpE,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;IAoBtB,IAAI;IAIJ,KAAK;IAIL,IAAI,CAAC,OAAO,EAAE,iBAAiB;IAyB/B,cAAc,CAAC,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS;CAuDtD"}
@@ -1,8 +1,8 @@
1
- import * as CBOR from "cbor-x";
2
1
  import debug from "debug";
3
2
  const log = debug("WebsocketServer");
4
- import { NetworkAdapter, } from "@automerge/automerge-repo";
5
3
  import { ProtocolV1 } from "./protocolVersion.js";
4
+ import { NetworkAdapter, cbor as cborHelpers, } from "@automerge/automerge-repo";
5
+ const { encode, decode } = cborHelpers;
6
6
  export class NodeWSServerAdapter extends NetworkAdapter {
7
7
  server;
8
8
  sockets = {};
@@ -23,6 +23,7 @@ export class NodeWSServerAdapter extends NetworkAdapter {
23
23
  }
24
24
  });
25
25
  socket.on("message", message => this.receiveMessage(message, socket));
26
+ this.emit("ready", { network: this });
26
27
  });
27
28
  }
28
29
  join() {
@@ -31,41 +32,32 @@ export class NodeWSServerAdapter extends NetworkAdapter {
31
32
  leave() {
32
33
  // throw new Error("The server doesn't join channels.")
33
34
  }
34
- sendMessage(targetId, channelId, message, broadcast) {
35
- if (message.byteLength === 0) {
35
+ send(message) {
36
+ if ("data" in message && message.data.byteLength === 0) {
36
37
  throw new Error("tried to send a zero-length message");
37
38
  }
38
39
  const senderId = this.peerId;
39
40
  if (!senderId) {
40
41
  throw new Error("No peerId set for the websocket server network adapter.");
41
42
  }
42
- if (this.sockets[targetId] === undefined) {
43
- log(`Tried to send message to disconnected peer: ${targetId}`);
43
+ if (this.sockets[message.targetId] === undefined) {
44
+ log(`Tried to send message to disconnected peer: ${message.targetId}`);
44
45
  return;
45
46
  }
46
- const decoded = {
47
- senderId,
48
- targetId,
49
- channelId,
50
- type: "sync",
51
- message,
52
- broadcast,
53
- };
54
- const encoded = CBOR.encode(decoded);
47
+ const encoded = encode(message);
55
48
  // This incantation deals with websocket sending the whole
56
49
  // underlying buffer even if we just have a uint8array view on it
57
50
  const arrayBuf = encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength);
58
- log(`[${senderId}->${targetId}@${channelId}] "sync" | ${arrayBuf.byteLength} bytes`);
59
- this.sockets[targetId].send(arrayBuf);
51
+ this.sockets[message.targetId]?.send(arrayBuf);
60
52
  }
61
53
  receiveMessage(message, socket) {
62
- const cbor = CBOR.decode(message);
63
- const { type, channelId, senderId, targetId, message: data, broadcast, supportedProtocolVersions, } = cbor;
54
+ const cbor = decode(message);
55
+ const { type, senderId } = cbor;
64
56
  const myPeerId = this.peerId;
65
57
  if (!myPeerId) {
66
58
  throw new Error("Missing my peer ID.");
67
59
  }
68
- log(`[${senderId}->${myPeerId}@${channelId}] ${type} | ${message.byteLength} bytes`);
60
+ log(`[${senderId}->${myPeerId}${"documentId" in cbor ? "@" + cbor.documentId : ""}] ${type} | ${message.byteLength} bytes`);
69
61
  switch (type) {
70
62
  case "join":
71
63
  // Let the rest of the system know that we have a new connection.
@@ -73,21 +65,24 @@ export class NodeWSServerAdapter extends NetworkAdapter {
73
65
  this.sockets[senderId] = socket;
74
66
  // In this client-server connection, there's only ever one peer: us!
75
67
  // (and we pretend to be joined to every channel)
76
- const selectedProtocolVersion = selectProtocol(supportedProtocolVersions);
68
+ const selectedProtocolVersion = selectProtocol(cbor.supportedProtocolVersions);
77
69
  if (selectedProtocolVersion === null) {
78
- socket.send(CBOR.encode({
70
+ this.send({
79
71
  type: "error",
80
- errorMessage: "unsupported protocol version",
81
- }));
72
+ senderId: this.peerId,
73
+ message: "unsupported protocol version",
74
+ targetId: senderId,
75
+ });
82
76
  this.sockets[senderId].close();
83
77
  delete this.sockets[senderId];
84
78
  }
85
79
  else {
86
- socket.send(CBOR.encode({
80
+ this.send({
87
81
  type: "peer",
88
82
  senderId: this.peerId,
89
83
  selectedProtocolVersion: ProtocolV1,
90
- }));
84
+ targetId: senderId,
85
+ });
91
86
  }
92
87
  break;
93
88
  case "leave":
@@ -96,21 +91,8 @@ export class NodeWSServerAdapter extends NetworkAdapter {
96
91
  // TODO: confirm this
97
92
  // ?
98
93
  break;
99
- // We accept both "message" and "sync" because a previous version of this
100
- // codebase sent sync messages in the BrowserWebSocketClientAdapter as
101
- // type "message" and we want to stay backwards compatible
102
- case "message":
103
- case "sync":
104
- this.emit("message", {
105
- senderId,
106
- targetId,
107
- channelId,
108
- message: new Uint8Array(data),
109
- broadcast,
110
- });
111
- break;
112
94
  default:
113
- log(`unrecognized message type ${type}`);
95
+ this.emit("message", cbor);
114
96
  break;
115
97
  }
116
98
  }
@@ -0,0 +1,26 @@
1
+ import { type Message, type PeerId } from "@automerge/automerge-repo";
2
+ import { ProtocolVersion } from "./protocolVersion.js";
3
+ export type LeaveMessage = {
4
+ type: "leave";
5
+ senderId: PeerId;
6
+ };
7
+ export type JoinMessage = {
8
+ type: "join";
9
+ senderId: PeerId;
10
+ supportedProtocolVersions: ProtocolVersion[];
11
+ };
12
+ export type PeerMessage = {
13
+ type: "peer";
14
+ senderId: PeerId;
15
+ selectedProtocolVersion: ProtocolVersion;
16
+ targetId: PeerId;
17
+ };
18
+ export type ErrorMessage = {
19
+ type: "error";
20
+ senderId: PeerId;
21
+ message: string;
22
+ targetId: PeerId;
23
+ };
24
+ export type FromClientMessage = JoinMessage | LeaveMessage | Message;
25
+ export type FromServerMessage = PeerMessage | ErrorMessage | Message;
26
+ //# sourceMappingURL=messages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,2BAA2B,CAAA;AACrE,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAEtD,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,OAAO,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,yBAAyB,EAAE,eAAe,EAAE,CAAA;CAC7C,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,uBAAuB,EAAE,eAAe,CAAA;IACxC,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,OAAO,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAGD,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,YAAY,GAAG,OAAO,CAAA;AACpE,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,YAAY,GAAG,OAAO,CAAA"}
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@automerge/automerge-repo-network-websocket",
3
- "version": "1.0.0-alpha.2",
3
+ "version": "1.0.0-alpha.4",
4
4
  "description": "isomorphic node/browser Websocket network adapter for Automerge Repo",
5
5
  "peerDependencies": {
6
- "@automerge/automerge": "^2.1.0-alpha.10"
6
+ "@automerge/automerge": "^2.1.0-alpha.12"
7
7
  },
8
8
  "repository": "https://github.com/automerge/automerge-repo",
9
9
  "author": "Peter van Hardenberg <pvh@pvh.ca>",
@@ -17,7 +17,7 @@
17
17
  "test": "mocha --no-warnings --experimental-specifier-resolution=node --exit"
18
18
  },
19
19
  "dependencies": {
20
- "@automerge/automerge-repo": "^1.0.0-alpha.2",
20
+ "@automerge/automerge-repo": "^1.0.0-alpha.4",
21
21
  "cbor-x": "^1.3.0",
22
22
  "eventemitter3": "^4.0.7",
23
23
  "isomorphic-ws": "^5.0.0",
@@ -34,5 +34,5 @@
34
34
  "publishConfig": {
35
35
  "access": "public"
36
36
  },
37
- "gitHead": "b5830dde8f135b694809698aaad2a9fdc79a9898"
37
+ "gitHead": "fbf71f0c3aaa2786a4e279f336f01d665f53ce5b"
38
38
  }
@@ -1,15 +1,16 @@
1
- import {
2
- ChannelId,
3
- InboundMessagePayload,
4
- NetworkAdapter,
5
- PeerId,
6
- } from "@automerge/automerge-repo"
7
- import * as CBOR from "cbor-x"
1
+ import { NetworkAdapter, PeerId, cbor } from "@automerge/automerge-repo"
8
2
  import WebSocket from "isomorphic-ws"
9
3
 
10
4
  import debug from "debug"
11
- import {ProtocolV1} from "./protocolVersion.js"
12
- import {InboundWebSocketMessage, OutboundWebSocketMessage} from "./message.js"
5
+
6
+ import {
7
+ FromClientMessage,
8
+ FromServerMessage,
9
+ JoinMessage,
10
+ } from "./messages.js"
11
+ import { ProtocolV1 } from "./protocolVersion.js"
12
+ import { isValidMessage } from "@automerge/automerge-repo/dist/network/messages.js"
13
+
13
14
  const log = debug("WebsocketClient")
14
15
 
15
16
  abstract class WebSocketNetworkAdapter extends NetworkAdapter {
@@ -17,9 +18,11 @@ abstract class WebSocketNetworkAdapter extends NetworkAdapter {
17
18
  }
18
19
 
19
20
  export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
20
- // Type trickery required for platform independence,
21
+ // Type trickery required for platform independence,
21
22
  // see https://stackoverflow.com/questions/45802988/typescript-use-correct-version-of-settimeout-node-vs-window
22
23
  timerId?: ReturnType<typeof setTimeout>
24
+ #startupComplete: boolean = false
25
+
23
26
  url: string
24
27
 
25
28
  constructor(url: string) {
@@ -55,6 +58,16 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
55
58
  this.socket.addEventListener("message", (event: WebSocket.MessageEvent) =>
56
59
  this.receiveMessage(event.data as Uint8Array)
57
60
  )
61
+
62
+ // mark this adapter as ready if we haven't received an ack in 1 second.
63
+ // We might hear back from the other end at some point but we shouldn't
64
+ // hold up marking things as unavailable for any longer
65
+ setTimeout(() => {
66
+ if (!this.#startupComplete) {
67
+ this.#startupComplete = true
68
+ this.emit("ready", { network: this })
69
+ }
70
+ }, 1000)
58
71
  }
59
72
 
60
73
  join() {
@@ -62,9 +75,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
62
75
  throw new Error("WTF, get a socket")
63
76
  }
64
77
  if (this.socket.readyState === WebSocket.OPEN) {
65
- this.socket.send(
66
- CBOR.encode(joinMessage(this.peerId))
67
- )
78
+ this.send(joinMessage(this.peerId!))
68
79
  } else {
69
80
  this.socket.addEventListener(
70
81
  "open",
@@ -72,9 +83,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
72
83
  if (!this.socket) {
73
84
  throw new Error("WTF, get a socket")
74
85
  }
75
- this.socket.send(
76
- CBOR.encode(joinMessage(this.peerId))
77
- )
86
+ this.send(joinMessage(this.peerId!))
78
87
  },
79
88
  { once: true }
80
89
  )
@@ -85,43 +94,31 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
85
94
  if (!this.socket) {
86
95
  throw new Error("WTF, get a socket")
87
96
  }
88
- this.socket.send(CBOR.encode({ type: "leave", senderId: this.peerId }))
97
+ this.send({ type: "leave", senderId: this.peerId! })
89
98
  }
90
99
 
91
- sendMessage(
92
- targetId: PeerId,
93
- channelId: ChannelId,
94
- message: Uint8Array,
95
- broadcast: boolean
96
- ) {
97
- if (message.byteLength === 0) {
100
+ send(message: FromClientMessage) {
101
+ if ("data" in message && message.data.byteLength === 0) {
98
102
  throw new Error("tried to send a zero-length message")
99
103
  }
104
+
100
105
  if (!this.peerId) {
101
106
  throw new Error("Why don't we have a PeerID?")
102
107
  }
103
108
 
104
- const decoded: InboundMessagePayload = {
105
- senderId: this.peerId,
106
- targetId,
107
- channelId,
108
- type: "sync",
109
- message,
110
- broadcast,
109
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
110
+ throw new Error("Websocket Socket not ready!")
111
111
  }
112
112
 
113
- const encoded = CBOR.encode(decoded)
114
-
113
+ const encoded = cbor.encode(message)
115
114
  // This incantation deals with websocket sending the whole
116
115
  // underlying buffer even if we just have a uint8array view on it
117
116
  const arrayBuf = encoded.buffer.slice(
118
117
  encoded.byteOffset,
119
118
  encoded.byteOffset + encoded.byteLength
120
119
  )
121
- if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
122
- throw new Error("Websocket Socket not ready!")
123
- }
124
- this.socket.send(arrayBuf)
120
+
121
+ this.socket?.send(arrayBuf)
125
122
  }
126
123
 
127
124
  announceConnection(peerId: PeerId) {
@@ -130,21 +127,17 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
130
127
  if (!myPeerId) {
131
128
  throw new Error("we should have a peer ID by now")
132
129
  }
133
-
130
+ if (!this.#startupComplete) {
131
+ this.#startupComplete = true
132
+ this.emit("ready", { network: this })
133
+ }
134
134
  this.emit("peer-candidate", { peerId })
135
135
  }
136
136
 
137
137
  receiveMessage(message: Uint8Array) {
138
- const decoded: OutboundWebSocketMessage = CBOR.decode(new Uint8Array(message))
138
+ const decoded: FromServerMessage = cbor.decode(new Uint8Array(message))
139
139
 
140
- const {
141
- type,
142
- senderId,
143
- targetId,
144
- channelId,
145
- message: messageData,
146
- broadcast,
147
- } = decoded
140
+ const { type, senderId } = decoded
148
141
 
149
142
  const socket = this.socket
150
143
  if (!socket) {
@@ -157,24 +150,22 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
157
150
 
158
151
  switch (type) {
159
152
  case "peer":
160
- log(`peer: ${senderId}, ${channelId}`)
153
+ log(`peer: ${senderId}`)
161
154
  this.announceConnection(senderId)
162
155
  break
163
156
  case "error":
164
- log(`error: ${decoded.errorMessage}`)
157
+ log(`error: ${decoded.message}`)
158
+ break
165
159
  default:
166
- this.emit("message", {
167
- channelId,
168
- senderId,
169
- targetId,
170
- message: new Uint8Array(messageData),
171
- broadcast,
172
- })
160
+ if (!isValidMessage(decoded)) {
161
+ throw new Error("Invalid message received")
162
+ }
163
+ this.emit("message", decoded)
173
164
  }
174
165
  }
175
166
  }
176
167
 
177
- function joinMessage(senderId?: PeerId): Record<string, any> {
168
+ function joinMessage(senderId: PeerId): JoinMessage {
178
169
  return {
179
170
  type: "join",
180
171
  senderId,
@@ -1,17 +1,18 @@
1
- import * as CBOR from "cbor-x"
2
1
  import { WebSocket, type WebSocketServer } from "isomorphic-ws"
3
2
 
4
3
  import debug from "debug"
5
4
  const log = debug("WebsocketServer")
6
5
 
6
+ import { ProtocolV1, ProtocolVersion } from "./protocolVersion.js"
7
7
  import {
8
- ChannelId,
9
- InboundMessagePayload,
10
8
  NetworkAdapter,
11
- PeerId,
9
+ cbor as cborHelpers,
10
+ type NetworkAdapterMessage,
11
+ type PeerId,
12
12
  } from "@automerge/automerge-repo"
13
- import { ProtocolV1, ProtocolVersion } from "./protocolVersion.js"
14
- import { InboundWebSocketMessage } from "./message.js"
13
+ import { FromClientMessage, FromServerMessage } from "./messages.js"
14
+
15
+ const { encode, decode } = cborHelpers
15
16
 
16
17
  export class NodeWSServerAdapter extends NetworkAdapter {
17
18
  server: WebSocketServer
@@ -38,6 +39,7 @@ export class NodeWSServerAdapter extends NetworkAdapter {
38
39
  socket.on("message", message =>
39
40
  this.receiveMessage(message as Uint8Array, socket)
40
41
  )
42
+ this.emit("ready", {network: this})
41
43
  })
42
44
  }
43
45
 
@@ -49,34 +51,21 @@ export class NodeWSServerAdapter extends NetworkAdapter {
49
51
  // throw new Error("The server doesn't join channels.")
50
52
  }
51
53
 
52
- sendMessage(
53
- targetId: PeerId,
54
- channelId: ChannelId,
55
- message: Uint8Array,
56
- broadcast: boolean
57
- ) {
58
- if (message.byteLength === 0) {
54
+ send(message: FromServerMessage) {
55
+ if ("data" in message && message.data.byteLength === 0) {
59
56
  throw new Error("tried to send a zero-length message")
60
57
  }
61
58
  const senderId = this.peerId
62
59
  if (!senderId) {
63
60
  throw new Error("No peerId set for the websocket server network adapter.")
64
61
  }
65
- if (this.sockets[targetId] === undefined) {
66
- log(`Tried to send message to disconnected peer: ${targetId}`)
67
- return
68
- }
69
62
 
70
- const decoded: InboundMessagePayload = {
71
- senderId,
72
- targetId,
73
- channelId,
74
- type: "sync",
75
- message,
76
- broadcast,
63
+ if (this.sockets[message.targetId] === undefined) {
64
+ log(`Tried to send message to disconnected peer: ${message.targetId}`)
65
+ return
77
66
  }
78
- const encoded = CBOR.encode(decoded)
79
67
 
68
+ const encoded = encode(message)
80
69
  // This incantation deals with websocket sending the whole
81
70
  // underlying buffer even if we just have a uint8array view on it
82
71
  const arrayBuf = encoded.buffer.slice(
@@ -84,32 +73,22 @@ export class NodeWSServerAdapter extends NetworkAdapter {
84
73
  encoded.byteOffset + encoded.byteLength
85
74
  )
86
75
 
87
- log(
88
- `[${senderId}->${targetId}@${channelId}] "sync" | ${arrayBuf.byteLength} bytes`
89
- )
90
-
91
- this.sockets[targetId].send(arrayBuf)
76
+ this.sockets[message.targetId]?.send(arrayBuf)
92
77
  }
93
78
 
94
79
  receiveMessage(message: Uint8Array, socket: WebSocket) {
95
- const cbor: InboundWebSocketMessage = CBOR.decode(message)
96
-
97
- const {
98
- type,
99
- channelId,
100
- senderId,
101
- targetId,
102
- message: data,
103
- broadcast,
104
- supportedProtocolVersions,
105
- } = cbor
80
+ const cbor: FromClientMessage = decode(message)
81
+
82
+ const { type, senderId } = cbor
106
83
 
107
84
  const myPeerId = this.peerId
108
85
  if (!myPeerId) {
109
86
  throw new Error("Missing my peer ID.")
110
87
  }
111
88
  log(
112
- `[${senderId}->${myPeerId}@${channelId}] ${type} | ${message.byteLength} bytes`
89
+ `[${senderId}->${myPeerId}${
90
+ "documentId" in cbor ? "@" + cbor.documentId : ""
91
+ }] ${type} | ${message.byteLength} bytes`
113
92
  )
114
93
  switch (type) {
115
94
  case "join":
@@ -120,25 +99,24 @@ export class NodeWSServerAdapter extends NetworkAdapter {
120
99
  // In this client-server connection, there's only ever one peer: us!
121
100
  // (and we pretend to be joined to every channel)
122
101
  const selectedProtocolVersion = selectProtocol(
123
- supportedProtocolVersions
102
+ cbor.supportedProtocolVersions
124
103
  )
125
104
  if (selectedProtocolVersion === null) {
126
- socket.send(
127
- CBOR.encode({
128
- type: "error",
129
- errorMessage: "unsupported protocol version",
130
- })
131
- )
105
+ this.send({
106
+ type: "error",
107
+ senderId: this.peerId!,
108
+ message: "unsupported protocol version",
109
+ targetId: senderId,
110
+ })
132
111
  this.sockets[senderId].close()
133
112
  delete this.sockets[senderId]
134
113
  } else {
135
- socket.send(
136
- CBOR.encode({
137
- type: "peer",
138
- senderId: this.peerId,
139
- selectedProtocolVersion: ProtocolV1,
140
- })
141
- )
114
+ this.send({
115
+ type: "peer",
116
+ senderId: this.peerId!,
117
+ selectedProtocolVersion: ProtocolV1,
118
+ targetId: senderId,
119
+ })
142
120
  }
143
121
  break
144
122
  case "leave":
@@ -148,21 +126,8 @@ export class NodeWSServerAdapter extends NetworkAdapter {
148
126
  // ?
149
127
  break
150
128
 
151
- // We accept both "message" and "sync" because a previous version of this
152
- // codebase sent sync messages in the BrowserWebSocketClientAdapter as
153
- // type "message" and we want to stay backwards compatible
154
- case "message":
155
- case "sync":
156
- this.emit("message", {
157
- senderId,
158
- targetId,
159
- channelId,
160
- message: new Uint8Array(data),
161
- broadcast,
162
- })
163
- break
164
129
  default:
165
- log(`unrecognized message type ${type}`)
130
+ this.emit("message", cbor)
166
131
  break
167
132
  }
168
133
  }
@@ -0,0 +1,31 @@
1
+ import { type Message, type PeerId } from "@automerge/automerge-repo"
2
+ import { ProtocolVersion } from "./protocolVersion.js"
3
+
4
+ export type LeaveMessage = {
5
+ type: "leave"
6
+ senderId: PeerId
7
+ }
8
+
9
+ export type JoinMessage = {
10
+ type: "join"
11
+ senderId: PeerId
12
+ supportedProtocolVersions: ProtocolVersion[]
13
+ }
14
+
15
+ export type PeerMessage = {
16
+ type: "peer"
17
+ senderId: PeerId
18
+ selectedProtocolVersion: ProtocolVersion
19
+ targetId: PeerId
20
+ }
21
+
22
+ export type ErrorMessage = {
23
+ type: "error"
24
+ senderId: PeerId
25
+ message: string
26
+ targetId: PeerId
27
+ }
28
+
29
+ // This adapter doesn't use NetworkAdapterMessage, it has its own idea of how to handle join/leave
30
+ export type FromClientMessage = JoinMessage | LeaveMessage | Message
31
+ export type FromServerMessage = PeerMessage | ErrorMessage | Message
@@ -1,19 +1,32 @@
1
- import { runAdapterTests } from "../../automerge-repo/src/helpers/tests/network-adapter-tests"
2
- import { BrowserWebSocketClientAdapter } from "../src/BrowserWebSocketClientAdapter"
3
- import { NodeWSServerAdapter } from "../src/NodeWSServerAdapter"
4
- import { startServer } from "./utilities/WebSockets"
1
+ import { runAdapterTests } from "../../automerge-repo/src/helpers/tests/network-adapter-tests.js"
2
+ import { BrowserWebSocketClientAdapter } from "../src/BrowserWebSocketClientAdapter.js"
3
+ import { NodeWSServerAdapter } from "../src/NodeWSServerAdapter.js"
4
+ import { startServer } from "./utilities/WebSockets.js"
5
5
  import * as CBOR from "cbor-x"
6
- import WebSocket, {AddressInfo} from "ws"
6
+ import WebSocket, { AddressInfo } from "ws"
7
7
  import { assert } from "chai"
8
- import {ChannelId, PeerId, Repo} from "@automerge/automerge-repo"
8
+ import { PeerId, Repo } from "@automerge/automerge-repo"
9
9
  import { once } from "events"
10
10
 
11
11
  describe("Websocket adapters", async () => {
12
12
  let port = 8080
13
13
 
14
14
  runAdapterTests(async () => {
15
- port += 1 // Increment port to avoid conflicts
16
- const { socket, server } = await startServer(port)
15
+ let socket: WebSocket.Server | undefined = undefined
16
+ let server: any
17
+
18
+ while (socket === undefined) {
19
+ try {
20
+ ; ({ socket, server } = await startServer(port))
21
+ } catch (e: any) {
22
+ if (e.code === "EADDRINUSE") {
23
+ port++
24
+ } else {
25
+ throw e
26
+ }
27
+ }
28
+ }
29
+
17
30
  const serverAdapter = new NodeWSServerAdapter(socket)
18
31
 
19
32
  const serverUrl = `ws://localhost:${port}`
@@ -25,19 +38,19 @@ describe("Websocket adapters", async () => {
25
38
  server.close()
26
39
  }
27
40
 
28
- return { adapters: [serverAdapter, aliceAdapter, bobAdapter], teardown }
41
+ return { adapters: [aliceAdapter, serverAdapter, bobAdapter], teardown }
29
42
  })
30
43
  })
31
44
 
32
45
  describe("The BrowserWebSocketClientAdapter", () => {
33
46
  it("should advertise the protocol versions it supports in its join message", async () => {
34
- const { socket, server } = await startServer(0)
47
+ const { socket, server } = await startServer(0)
35
48
  let port = (server.address()!! as AddressInfo).port
36
49
  const serverUrl = `ws://localhost:${port}`
37
50
  const helloPromise = firstMessage(socket)
38
51
 
39
52
  const client = new BrowserWebSocketClientAdapter(serverUrl)
40
- const repo = new Repo({network: [client], peerId: "browser" as PeerId})
53
+ const repo = new Repo({ network: [client], peerId: "browser" as PeerId })
41
54
 
42
55
  const hello = await helloPromise
43
56
 
@@ -45,8 +58,8 @@ describe("The BrowserWebSocketClientAdapter", () => {
45
58
  assert.deepEqual(message, {
46
59
  type: "join",
47
60
  senderId: "browser",
48
- supportedProtocolVersions: ["1"]
49
- })
61
+ supportedProtocolVersions: ["1"],
62
+ })
50
63
  })
51
64
  })
52
65
 
@@ -55,12 +68,13 @@ describe("The NodeWSServerAdapter", () => {
55
68
  const response = await serverHelloGivenClientHello({
56
69
  type: "join",
57
70
  senderId: "browser",
58
- supportedProtocolVersions: ["1"]
71
+ supportedProtocolVersions: ["1"],
59
72
  })
60
73
  assert.deepEqual<any>(response, {
61
74
  type: "peer",
62
75
  senderId: "server",
63
- selectedProtocolVersion: "1"
76
+ targetId: "browser",
77
+ selectedProtocolVersion: "1",
64
78
  })
65
79
  })
66
80
 
@@ -68,11 +82,13 @@ describe("The NodeWSServerAdapter", () => {
68
82
  const response = await serverHelloGivenClientHello({
69
83
  type: "join",
70
84
  senderId: "browser",
71
- supportedProtocolVersions: ["fake"]
85
+ supportedProtocolVersions: ["fake"],
72
86
  })
73
87
  assert.deepEqual<any>(response, {
74
88
  type: "error",
75
- errorMessage: "unsupported protocol version",
89
+ senderId: "server",
90
+ targetId: "browser",
91
+ message: "unsupported protocol version",
76
92
  })
77
93
  })
78
94
 
@@ -84,17 +100,20 @@ describe("The NodeWSServerAdapter", () => {
84
100
  assert.deepEqual<any>(response, {
85
101
  type: "peer",
86
102
  senderId: "server",
87
- selectedProtocolVersion: "1"
103
+ targetId: "browser",
104
+ selectedProtocolVersion: "1",
88
105
  })
89
106
  })
90
107
  })
91
108
 
92
- async function serverHelloGivenClientHello(clientHello: Object): Promise<Object | null> {
93
- const { socket, server } = await startServer(0)
109
+ async function serverHelloGivenClientHello(
110
+ clientHello: Object
111
+ ): Promise<Object | null> {
112
+ const { socket, server } = await startServer(0)
94
113
  let port = (server.address()!! as AddressInfo).port
95
114
  const serverUrl = `ws://localhost:${port}`
96
115
  const adapter = new NodeWSServerAdapter(socket)
97
- const repo = new Repo({network: [adapter], peerId: "server" as PeerId})
116
+ const repo = new Repo({ network: [adapter], peerId: "server" as PeerId })
98
117
 
99
118
  const clientSocket = new WebSocket(serverUrl)
100
119
  await once(clientSocket, "open")
@@ -107,9 +126,11 @@ async function serverHelloGivenClientHello(clientHello: Object): Promise<Object
107
126
  return message
108
127
  }
109
128
 
110
- async function firstMessage(socket: WebSocket.Server<any>): Promise<Object | null> {
129
+ async function firstMessage(
130
+ socket: WebSocket.Server<any>
131
+ ): Promise<Object | null> {
111
132
  return new Promise((resolve, reject) => {
112
- socket.once("connection", (ws) => {
133
+ socket.once("connection", ws => {
113
134
  ws.once("message", (message: any) => {
114
135
  resolve(message)
115
136
  })
@@ -117,7 +138,7 @@ async function firstMessage(socket: WebSocket.Server<any>): Promise<Object | nul
117
138
  reject(error)
118
139
  })
119
140
  })
120
- socket.once("error", (error) => {
141
+ socket.once("error", error => {
121
142
  reject(error)
122
143
  })
123
144
  })
@@ -1,5 +1,5 @@
1
1
  import http from "http"
2
- import { createWebSocketServer } from "./CreateWebSocketServer"
2
+ import { createWebSocketServer } from "./CreateWebSocketServer.js"
3
3
  import WebSocket from "ws"
4
4
 
5
5
  function startServer(port: number) {
package/tsconfig.json CHANGED
@@ -2,8 +2,8 @@
2
2
  "compilerOptions": {
3
3
  "target": "ESNext",
4
4
  "jsx": "react",
5
- "module": "ESNext",
6
- "moduleResolution": "node",
5
+ "module": "NodeNext",
6
+ "moduleResolution": "Node16",
7
7
  "declaration": true,
8
8
  "declarationMap": true,
9
9
  "outDir": "./dist",
package/dist/message.d.ts DELETED
@@ -1,9 +0,0 @@
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
@@ -1 +0,0 @@
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/src/message.ts DELETED
@@ -1,10 +0,0 @@
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
- }
File without changes