@automerge/automerge-repo-network-websocket 1.0.19 → 1.1.0-alpha.13

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,5 +1,5 @@
1
1
  /// <reference types="ws" />
2
- import { NetworkAdapter, PeerId } from "@automerge/automerge-repo";
2
+ import { NetworkAdapter, PeerId, PeerMetadata } from "@automerge/automerge-repo";
3
3
  import WebSocket from "isomorphic-ws";
4
4
  import { FromClientMessage } from "./messages.js";
5
5
  declare abstract class WebSocketNetworkAdapter extends NetworkAdapter {
@@ -7,19 +7,20 @@ declare abstract class WebSocketNetworkAdapter extends NetworkAdapter {
7
7
  }
8
8
  export declare class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
9
9
  #private;
10
- timerId?: ReturnType<typeof setTimeout>;
10
+ readonly url: string;
11
+ readonly retryInterval: number;
11
12
  remotePeerId?: PeerId;
12
- url: string;
13
- constructor(url: string);
14
- connect(peerId: PeerId): void;
13
+ constructor(url: string, retryInterval?: number);
14
+ connect(peerId: PeerId, peerMetadata?: PeerMetadata): void;
15
15
  onOpen: () => void;
16
16
  onClose: () => void;
17
17
  onMessage: (event: WebSocket.MessageEvent) => void;
18
+ onError: (event: WebSocket.ErrorEvent) => void;
18
19
  join(): void;
19
20
  disconnect(): void;
20
21
  send(message: FromClientMessage): void;
21
- announceConnection(peerId: PeerId): void;
22
- receiveMessage(message: Uint8Array): void;
22
+ peerCandidate(remotePeerId: PeerId, peerMetadata: PeerMetadata): void;
23
+ receiveMessage(messageBytes: Uint8Array): void;
23
24
  }
24
25
  export {};
25
26
  //# sourceMappingURL=BrowserWebSocketClientAdapter.d.ts.map
@@ -1 +1 @@
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;AAKtB,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,YAAY,CAAC,EAAE,MAAM,CAAA;IAGrB,GAAG,EAAE,MAAM,CAAA;gBAEC,GAAG,EAAE,MAAM;IAKvB,OAAO,CAAC,MAAM,EAAE,MAAM;IAkCtB,MAAM,aAKL;IAGD,OAAO,aAYN;IAED,SAAS,UAAW,sBAAsB,UAEzC;IAED,IAAI;IAWJ,UAAU;IAOV,IAAI,CAAC,OAAO,EAAE,iBAAiB;IAwB/B,kBAAkB,CAAC,MAAM,EAAE,MAAM;IAcjC,cAAc,CAAC,OAAO,EAAE,UAAU;CA0BnC"}
1
+ {"version":3,"file":"BrowserWebSocketClientAdapter.d.ts","sourceRoot":"","sources":["../src/BrowserWebSocketClientAdapter.ts"],"names":[],"mappings":";AAAA,OAAO,EACL,cAAc,EACd,MAAM,EACN,YAAY,EAEb,MAAM,2BAA2B,CAAA;AAClC,OAAO,SAAS,MAAM,eAAe,CAAA;AAIrC,OAAO,EACL,iBAAiB,EAKlB,MAAM,eAAe,CAAA;AAKtB,uBAAe,uBAAwB,SAAQ,cAAc;IAC3D,MAAM,CAAC,EAAE,SAAS,CAAA;CACnB;AAED,qBAAa,6BAA8B,SAAQ,uBAAuB;;aAQtD,GAAG,EAAE,MAAM;aACX,aAAa;IAJ/B,YAAY,CAAC,EAAE,MAAM,CAAA;gBAGH,GAAG,EAAE,MAAM,EACX,aAAa,SAAO;IAMtC,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,YAAY;IAqCnD,MAAM,aAKL;IAGD,OAAO,aAWN;IAED,SAAS,UAAW,sBAAsB,UAEzC;IAED,OAAO,UAAW,oBAAoB,UAQrC;IAQD,IAAI;IAUJ,UAAU;IAMV,IAAI,CAAC,OAAO,EAAE,iBAAiB;IAY/B,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY;IAU9D,cAAc,CAAC,YAAY,EAAE,UAAU;CAiBxC"}
@@ -1,144 +1,153 @@
1
- import { NetworkAdapter, cbor } from "@automerge/automerge-repo";
1
+ import { NetworkAdapter, cbor, } from "@automerge/automerge-repo";
2
2
  import WebSocket from "isomorphic-ws";
3
3
  import debug from "debug";
4
+ import { isErrorMessage, isPeerMessage, } from "./messages.js";
4
5
  import { ProtocolV1 } from "./protocolVersion.js";
5
- const log = debug("WebsocketClient");
6
+ import { assert } from "./assert.js";
7
+ import { toArrayBuffer } from "./toArrayBuffer.js";
6
8
  class WebSocketNetworkAdapter extends NetworkAdapter {
7
9
  socket;
8
10
  }
9
11
  export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
10
- // Type trickery required for platform independence,
11
- // see https://stackoverflow.com/questions/45802988/typescript-use-correct-version-of-settimeout-node-vs-window
12
- timerId;
13
- remotePeerId; // this adapter only connects to one remote client at a time
14
- #startupComplete = false;
15
12
  url;
16
- constructor(url) {
13
+ retryInterval;
14
+ #isReady = false;
15
+ #retryIntervalId;
16
+ #log = debug("automerge-repo:websocket:browser");
17
+ remotePeerId; // this adapter only connects to one remote client at a time
18
+ constructor(url, retryInterval = 5000) {
17
19
  super();
18
20
  this.url = url;
21
+ this.retryInterval = retryInterval;
22
+ this.#log = this.#log.extend(url);
19
23
  }
20
- connect(peerId) {
21
- // If we're reconnecting make sure we remove the old event listeners
22
- // before creating a new connection.
23
- if (this.socket) {
24
+ connect(peerId, peerMetadata) {
25
+ if (!this.socket || !this.peerId) {
26
+ // first time connecting
27
+ this.#log("connecting");
28
+ this.peerId = peerId;
29
+ this.peerMetadata = peerMetadata ?? {};
30
+ }
31
+ else {
32
+ this.#log("reconnecting");
33
+ assert(peerId === this.peerId);
34
+ // Remove the old event listeners before creating a new connection.
24
35
  this.socket.removeEventListener("open", this.onOpen);
25
36
  this.socket.removeEventListener("close", this.onClose);
26
37
  this.socket.removeEventListener("message", this.onMessage);
38
+ this.socket.removeEventListener("error", this.onError);
27
39
  }
28
- if (!this.timerId) {
29
- this.timerId = setInterval(() => this.connect(peerId), 5000);
30
- }
31
- this.peerId = peerId;
40
+ // Wire up retries
41
+ if (!this.#retryIntervalId)
42
+ this.#retryIntervalId = setInterval(() => {
43
+ this.connect(peerId, peerMetadata);
44
+ }, this.retryInterval);
32
45
  this.socket = new WebSocket(this.url);
33
46
  this.socket.binaryType = "arraybuffer";
34
47
  this.socket.addEventListener("open", this.onOpen);
35
48
  this.socket.addEventListener("close", this.onClose);
36
49
  this.socket.addEventListener("message", this.onMessage);
37
- // mark this adapter as ready if we haven't received an ack in 1 second.
50
+ this.socket.addEventListener("error", this.onError);
51
+ // Mark this adapter as ready if we haven't received an ack in 1 second.
38
52
  // We might hear back from the other end at some point but we shouldn't
39
53
  // hold up marking things as unavailable for any longer
40
- setTimeout(() => {
41
- if (!this.#startupComplete) {
42
- this.#startupComplete = true;
43
- this.emit("ready", { network: this });
44
- }
45
- }, 1000);
54
+ setTimeout(() => this.#ready(), 1000);
46
55
  this.join();
47
56
  }
48
57
  onOpen = () => {
49
- log(`@ ${this.url}: open`);
50
- clearInterval(this.timerId);
51
- this.timerId = undefined;
52
- this.send(joinMessage(this.peerId));
58
+ this.#log("open");
59
+ clearInterval(this.#retryIntervalId);
60
+ this.#retryIntervalId = undefined;
61
+ this.join();
53
62
  };
54
63
  // When a socket closes, or disconnects, remove it from the array.
55
64
  onClose = () => {
56
- log(`${this.url}: close`);
57
- if (this.remotePeerId) {
65
+ this.#log("close");
66
+ if (this.remotePeerId)
58
67
  this.emit("peer-disconnected", { peerId: this.remotePeerId });
59
- }
60
- if (!this.timerId) {
61
- if (this.peerId) {
62
- this.connect(this.peerId);
63
- }
64
- }
68
+ if (this.retryInterval > 0 && !this.#retryIntervalId)
69
+ // try to reconnect
70
+ setTimeout(() => {
71
+ assert(this.peerId);
72
+ return this.connect(this.peerId, this.peerMetadata);
73
+ }, this.retryInterval);
65
74
  };
66
75
  onMessage = (event) => {
67
76
  this.receiveMessage(event.data);
68
77
  };
69
- join() {
70
- if (!this.socket) {
71
- throw new Error("WTF, get a socket");
78
+ onError = (event) => {
79
+ const { code } = event.error;
80
+ if (code === "ECONNREFUSED") {
81
+ this.#log("Connection refused, retrying...");
72
82
  }
83
+ else {
84
+ /* c8 ignore next */
85
+ throw event.error;
86
+ }
87
+ };
88
+ #ready() {
89
+ if (this.#isReady)
90
+ return;
91
+ this.#isReady = true;
92
+ this.emit("ready", { network: this });
93
+ }
94
+ join() {
95
+ assert(this.peerId);
96
+ assert(this.socket);
73
97
  if (this.socket.readyState === WebSocket.OPEN) {
74
- this.send(joinMessage(this.peerId));
98
+ this.send(joinMessage(this.peerId, this.peerMetadata));
75
99
  }
76
100
  else {
77
- // The onOpen handler automatically sends a join message
101
+ // We'll try again in the `onOpen` handler
78
102
  }
79
103
  }
80
104
  disconnect() {
81
- if (!this.socket) {
82
- throw new Error("WTF, get a socket");
83
- }
105
+ assert(this.peerId);
106
+ assert(this.socket);
84
107
  this.send({ type: "leave", senderId: this.peerId });
85
108
  }
86
109
  send(message) {
87
- if ("data" in message && message.data.byteLength === 0) {
88
- throw new Error("tried to send a zero-length message");
89
- }
90
- if (!this.peerId) {
91
- throw new Error("Why don't we have a PeerID?");
92
- }
93
- if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
94
- throw new Error("Websocket Socket not ready!");
95
- }
110
+ if ("data" in message && message.data?.byteLength === 0)
111
+ throw new Error("Tried to send a zero-length message");
112
+ assert(this.peerId);
113
+ assert(this.socket);
114
+ if (this.socket.readyState !== WebSocket.OPEN)
115
+ throw new Error(`Websocket not ready (${this.socket.readyState})`);
96
116
  const encoded = cbor.encode(message);
97
- // This incantation deals with websocket sending the whole
98
- // underlying buffer even if we just have a uint8array view on it
99
- const arrayBuf = encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength);
100
- this.socket?.send(arrayBuf);
117
+ this.socket.send(toArrayBuffer(encoded));
101
118
  }
102
- announceConnection(peerId) {
103
- // return a peer object
104
- const myPeerId = this.peerId;
105
- if (!myPeerId) {
106
- throw new Error("we should have a peer ID by now");
107
- }
108
- if (!this.#startupComplete) {
109
- this.#startupComplete = true;
110
- this.emit("ready", { network: this });
111
- }
112
- this.remotePeerId = peerId;
113
- this.emit("peer-candidate", { peerId });
119
+ peerCandidate(remotePeerId, peerMetadata) {
120
+ assert(this.socket);
121
+ this.#ready();
122
+ this.remotePeerId = remotePeerId;
123
+ this.emit("peer-candidate", {
124
+ peerId: remotePeerId,
125
+ peerMetadata,
126
+ });
114
127
  }
115
- receiveMessage(message) {
116
- const decoded = cbor.decode(new Uint8Array(message));
117
- const { type, senderId } = decoded;
118
- const socket = this.socket;
119
- if (!socket) {
120
- throw new Error("Missing socket at receiveMessage");
121
- }
122
- if (message.byteLength === 0) {
128
+ receiveMessage(messageBytes) {
129
+ const message = cbor.decode(new Uint8Array(messageBytes));
130
+ assert(this.socket);
131
+ if (messageBytes.byteLength === 0)
123
132
  throw new Error("received a zero-length message");
133
+ if (isPeerMessage(message)) {
134
+ const { peerMetadata } = message;
135
+ this.#log(`peer: ${message.senderId}`);
136
+ this.peerCandidate(message.senderId, peerMetadata);
124
137
  }
125
- switch (type) {
126
- case "peer":
127
- log(`peer: ${senderId}`);
128
- this.announceConnection(senderId);
129
- break;
130
- case "error":
131
- log(`error: ${decoded.message}`);
132
- break;
133
- default:
134
- this.emit("message", decoded);
138
+ else if (isErrorMessage(message)) {
139
+ this.#log(`error: ${message.message}`);
140
+ }
141
+ else {
142
+ this.emit("message", message);
135
143
  }
136
144
  }
137
145
  }
138
- function joinMessage(senderId) {
146
+ function joinMessage(senderId, peerMetadata) {
139
147
  return {
140
148
  type: "join",
141
149
  senderId,
150
+ peerMetadata,
142
151
  supportedProtocolVersions: [ProtocolV1],
143
152
  };
144
153
  }
@@ -1,17 +1,19 @@
1
1
  /// <reference types="ws" />
2
2
  import WebSocket from "isomorphic-ws";
3
3
  import { type WebSocketServer } from "isomorphic-ws";
4
- import { NetworkAdapter, type PeerId } from "@automerge/automerge-repo";
4
+ import { NetworkAdapter, type PeerMetadata, type PeerId } from "@automerge/automerge-repo";
5
5
  import { FromServerMessage } from "./messages.js";
6
6
  export declare class NodeWSServerAdapter extends NetworkAdapter {
7
- server: WebSocketServer;
7
+ #private;
8
+ private server;
9
+ private keepAliveInterval;
8
10
  sockets: {
9
11
  [peerId: PeerId]: WebSocket;
10
12
  };
11
- constructor(server: WebSocketServer);
12
- connect(peerId: PeerId): void;
13
+ constructor(server: WebSocketServer, keepAliveInterval?: number);
14
+ connect(peerId: PeerId, peerMetadata: PeerMetadata): void;
13
15
  disconnect(): void;
14
16
  send(message: FromServerMessage): void;
15
- receiveMessage(message: Uint8Array, socket: WebSocket): void;
17
+ receiveMessage(messageBytes: Uint8Array, socket: WebSocket): void;
16
18
  }
17
19
  //# sourceMappingURL=NodeWSServerAdapter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"NodeWSServerAdapter.d.ts","sourceRoot":"","sources":["../src/NodeWSServerAdapter.ts"],"names":[],"mappings":";AAAA,OAAO,SAAS,MAAM,eAAe,CAAA;AACrC,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAA;AAKpD,OAAO,EAEL,cAAc,EACd,KAAK,MAAM,EACZ,MAAM,2BAA2B,CAAA;AAClC,OAAO,EAAqB,iBAAiB,EAAE,MAAM,eAAe,CAAA;AASpE,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;IAoDtB,UAAU;IAIV,IAAI,CAAC,OAAO,EAAE,iBAAiB;IAyB/B,cAAc,CAAC,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS;CA+DtD"}
1
+ {"version":3,"file":"NodeWSServerAdapter.d.ts","sourceRoot":"","sources":["../src/NodeWSServerAdapter.ts"],"names":[],"mappings":";AAAA,OAAO,SAAS,MAAM,eAAe,CAAA;AACrC,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAA;AAKpD,OAAO,EAEL,cAAc,EACd,KAAK,YAAY,EACjB,KAAK,MAAM,EACZ,MAAM,2BAA2B,CAAA;AAClC,OAAO,EAEL,iBAAiB,EAGlB,MAAM,eAAe,CAAA;AAOtB,qBAAa,mBAAoB,SAAQ,cAAc;;IAInD,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,iBAAiB;IAJ3B,OAAO,EAAE;QAAE,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAK;gBAGnC,MAAM,EAAE,eAAe,EACvB,iBAAiB,SAAO;IAKlC,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY;IAyClD,UAAU;IAQV,IAAI,CAAC,OAAO,EAAE,iBAAiB;IAqB/B,cAAc,CAAC,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS;CA0E3D"}
@@ -2,29 +2,31 @@ import WebSocket from "isomorphic-ws";
2
2
  import debug from "debug";
3
3
  const log = debug("WebsocketServer");
4
4
  import { cbor as cborHelpers, NetworkAdapter, } from "@automerge/automerge-repo";
5
+ import { isJoinMessage, isLeaveMessage, } from "./messages.js";
5
6
  import { ProtocolV1 } from "./protocolVersion.js";
7
+ import assert from "assert";
8
+ import { toArrayBuffer } from "./toArrayBuffer.js";
6
9
  const { encode, decode } = cborHelpers;
7
10
  export class NodeWSServerAdapter extends NetworkAdapter {
8
11
  server;
12
+ keepAliveInterval;
9
13
  sockets = {};
10
- constructor(server) {
14
+ constructor(server, keepAliveInterval = 5000) {
11
15
  super();
12
16
  this.server = server;
17
+ this.keepAliveInterval = keepAliveInterval;
13
18
  }
14
- connect(peerId) {
19
+ connect(peerId, peerMetadata) {
15
20
  this.peerId = peerId;
16
- this.server.on("close", function close() {
17
- clearInterval(interval);
21
+ this.peerMetadata = peerMetadata;
22
+ this.server.on("close", () => {
23
+ clearInterval(keepAliveId);
24
+ this.disconnect();
18
25
  });
19
26
  this.server.on("connection", (socket) => {
20
27
  // When a socket closes, or disconnects, remove it from our list
21
28
  socket.on("close", () => {
22
- for (const [otherPeerId, otherSocket] of Object.entries(this.sockets)) {
23
- if (socket === otherSocket) {
24
- this.emit("peer-disconnected", { peerId: otherPeerId });
25
- delete this.sockets[otherPeerId];
26
- }
27
- }
29
+ this.#removeSocket(socket);
28
30
  });
29
31
  socket.on("message", message => this.receiveMessage(message, socket));
30
32
  // Start out "alive", and every time we get a pong, reset that state.
@@ -32,108 +34,117 @@ export class NodeWSServerAdapter extends NetworkAdapter {
32
34
  socket.on("pong", () => (socket.isAlive = true));
33
35
  this.emit("ready", { network: this });
34
36
  });
35
- // Every interval, terminate connections to lost clients,
36
- // then mark all clients as potentially dead and then ping them.
37
- const interval = setInterval(() => {
38
- ;
39
- this.server.clients.forEach(socket => {
40
- if (socket.isAlive === false) {
41
- // Make sure we clean up this socket even though we're terminating.
42
- // This might be unnecessary but I have read reports of the close() not happening for 30s.
43
- for (const [otherPeerId, otherSocket] of Object.entries(this.sockets)) {
44
- if (socket === otherSocket) {
45
- this.emit("peer-disconnected", { peerId: otherPeerId });
46
- delete this.sockets[otherPeerId];
47
- }
48
- }
49
- return socket.terminate();
37
+ const keepAliveId = setInterval(() => {
38
+ // Terminate connections to lost clients
39
+ const clients = this.server.clients;
40
+ clients.forEach(socket => {
41
+ if (socket.isAlive) {
42
+ // Mark all clients as potentially dead until we hear from them
43
+ socket.isAlive = false;
44
+ socket.ping();
45
+ }
46
+ else {
47
+ this.#terminate(socket);
50
48
  }
51
- socket.isAlive = false;
52
- socket.ping();
53
49
  });
54
- }, 5000);
50
+ }, this.keepAliveInterval);
55
51
  }
56
52
  disconnect() {
57
- // throw new Error("The server doesn't join channels.")
53
+ const clients = this.server.clients;
54
+ clients.forEach(socket => {
55
+ this.#terminate(socket);
56
+ this.#removeSocket(socket);
57
+ });
58
58
  }
59
59
  send(message) {
60
- if ("data" in message && message.data.byteLength === 0) {
61
- throw new Error("tried to send a zero-length message");
62
- }
60
+ assert("targetId" in message && message.targetId !== undefined);
61
+ if ("data" in message && message.data?.byteLength === 0)
62
+ throw new Error("Tried to send a zero-length message");
63
63
  const senderId = this.peerId;
64
- if (!senderId) {
65
- throw new Error("No peerId set for the websocket server network adapter.");
66
- }
67
- if (this.sockets[message.targetId] === undefined) {
68
- log(`Tried to send message to disconnected peer: ${message.targetId}`);
64
+ assert(senderId, "No peerId set for the websocket server network adapter.");
65
+ const socket = this.sockets[message.targetId];
66
+ if (!socket) {
67
+ log(`Tried to send to disconnected peer: ${message.targetId}`);
69
68
  return;
70
69
  }
71
70
  const encoded = encode(message);
72
- // This incantation deals with websocket sending the whole
73
- // underlying buffer even if we just have a uint8array view on it
74
- const arrayBuf = encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength);
75
- this.sockets[message.targetId]?.send(arrayBuf);
71
+ const arrayBuf = toArrayBuffer(encoded);
72
+ socket.send(arrayBuf);
76
73
  }
77
- receiveMessage(message, socket) {
78
- const cbor = decode(message);
79
- const { type, senderId } = cbor;
74
+ receiveMessage(messageBytes, socket) {
75
+ const message = decode(messageBytes);
76
+ const { type, senderId } = message;
80
77
  const myPeerId = this.peerId;
81
- if (!myPeerId) {
82
- throw new Error("Missing my peer ID.");
83
- }
84
- log(`[${senderId}->${myPeerId}${"documentId" in cbor ? "@" + cbor.documentId : ""}] ${type} | ${message.byteLength} bytes`);
85
- switch (type) {
86
- case "join":
87
- const existingSocket = this.sockets[senderId];
88
- if (existingSocket) {
89
- if (existingSocket.readyState === WebSocket.OPEN) {
90
- existingSocket.close();
91
- }
92
- this.emit("peer-disconnected", { peerId: senderId });
93
- }
94
- // Let the rest of the system know that we have a new connection.
95
- this.emit("peer-candidate", { peerId: senderId });
96
- this.sockets[senderId] = socket;
97
- // In this client-server connection, there's only ever one peer: us!
98
- // (and we pretend to be joined to every channel)
99
- const selectedProtocolVersion = selectProtocol(cbor.supportedProtocolVersions);
100
- if (selectedProtocolVersion === null) {
101
- this.send({
102
- type: "error",
103
- senderId: this.peerId,
104
- message: "unsupported protocol version",
105
- targetId: senderId,
106
- });
107
- this.sockets[senderId].close();
108
- delete this.sockets[senderId];
109
- }
110
- else {
111
- this.send({
112
- type: "peer",
113
- senderId: this.peerId,
114
- selectedProtocolVersion: ProtocolV1,
115
- targetId: senderId,
116
- });
78
+ assert(myPeerId);
79
+ const documentId = "documentId" in message ? "@" + message.documentId : "";
80
+ const { byteLength } = messageBytes;
81
+ log(`[${senderId}->${myPeerId}${documentId}] ${type} | ${byteLength} bytes`);
82
+ if (isJoinMessage(message)) {
83
+ const { peerMetadata, supportedProtocolVersions } = message;
84
+ const existingSocket = this.sockets[senderId];
85
+ if (existingSocket) {
86
+ if (existingSocket.readyState === WebSocket.OPEN) {
87
+ existingSocket.close();
117
88
  }
118
- break;
119
- case "leave":
120
- // It doesn't seem like this gets called;
121
- // we handle leaving in the socket close logic
122
- // TODO: confirm this
123
- // ?
124
- break;
125
- default:
126
- this.emit("message", cbor);
127
- break;
89
+ this.emit("peer-disconnected", { peerId: senderId });
90
+ }
91
+ // Let the repo know that we have a new connection.
92
+ this.emit("peer-candidate", { peerId: senderId, peerMetadata });
93
+ this.sockets[senderId] = socket;
94
+ const selectedProtocolVersion = selectProtocol(supportedProtocolVersions);
95
+ if (selectedProtocolVersion === null) {
96
+ this.send({
97
+ type: "error",
98
+ senderId: this.peerId,
99
+ message: "unsupported protocol version",
100
+ targetId: senderId,
101
+ });
102
+ this.sockets[senderId].close();
103
+ delete this.sockets[senderId];
104
+ }
105
+ else {
106
+ this.send({
107
+ type: "peer",
108
+ senderId: this.peerId,
109
+ peerMetadata: this.peerMetadata,
110
+ selectedProtocolVersion: ProtocolV1,
111
+ targetId: senderId,
112
+ });
113
+ }
114
+ }
115
+ else if (isLeaveMessage(message)) {
116
+ const { senderId } = message;
117
+ const socket = this.sockets[senderId];
118
+ /* c8 ignore next */
119
+ if (!socket)
120
+ return;
121
+ this.#terminate(socket);
122
+ }
123
+ else {
124
+ this.emit("message", message);
128
125
  }
129
126
  }
127
+ #terminate(socket) {
128
+ this.#removeSocket(socket);
129
+ socket.terminate();
130
+ }
131
+ #removeSocket(socket) {
132
+ const peerId = this.#peerIdBySocket(socket);
133
+ if (!peerId)
134
+ return;
135
+ this.emit("peer-disconnected", { peerId });
136
+ delete this.sockets[peerId];
137
+ }
138
+ #peerIdBySocket = (socket) => {
139
+ const isThisSocket = (peerId) => this.sockets[peerId] === socket;
140
+ const result = Object.keys(this.sockets).find(isThisSocket);
141
+ return result ?? null;
142
+ };
130
143
  }
131
- function selectProtocol(versions) {
132
- if (versions === undefined) {
144
+ const selectProtocol = (versions) => {
145
+ if (versions === undefined)
133
146
  return ProtocolV1;
134
- }
135
- if (versions.includes(ProtocolV1)) {
147
+ if (versions.includes(ProtocolV1))
136
148
  return ProtocolV1;
137
- }
138
149
  return null;
139
- }
150
+ };
@@ -0,0 +1,3 @@
1
+ export declare function assert(value: boolean, message?: string): asserts value;
2
+ export declare function assert<T>(value: T | undefined, message?: string): asserts value is T;
3
+ //# sourceMappingURL=assert.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assert.d.ts","sourceRoot":"","sources":["../src/assert.ts"],"names":[],"mappings":"AAEA,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAA;AACvE,wBAAgB,MAAM,CAAC,CAAC,EACtB,KAAK,EAAE,CAAC,GAAG,SAAS,EACpB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,KAAK,IAAI,CAAC,CAAA"}
package/dist/assert.js ADDED
@@ -0,0 +1,17 @@
1
+ /* c8 ignore start */
2
+ export function assert(value, message = "Assertion failed") {
3
+ if (value === false || value === null || value === undefined) {
4
+ const error = new Error(trimLines(message));
5
+ error.stack = removeLine(error.stack, "assert.ts");
6
+ throw error;
7
+ }
8
+ }
9
+ const trimLines = (s) => s
10
+ .split("\n")
11
+ .map(s => s.trim())
12
+ .join("\n");
13
+ const removeLine = (s = "", targetText) => s
14
+ .split("\n")
15
+ .filter(line => !line.includes(targetText))
16
+ .join("\n");
17
+ /* c8 ignore end */
@@ -1,4 +1,4 @@
1
- import type { Message, PeerId } from "@automerge/automerge-repo";
1
+ import type { Message, PeerId, PeerMetadata } from "@automerge/automerge-repo";
2
2
  import type { ProtocolVersion } from "./protocolVersion.js";
3
3
  /** The sender is disconnecting */
4
4
  export type LeaveMessage = {
@@ -10,6 +10,8 @@ export type JoinMessage = {
10
10
  type: "join";
11
11
  /** The PeerID of the client */
12
12
  senderId: PeerId;
13
+ /** Metadata presented by the peer */
14
+ peerMetadata: PeerMetadata;
13
15
  /** The protocol version the client supports */
14
16
  supportedProtocolVersions: ProtocolVersion[];
15
17
  };
@@ -18,6 +20,8 @@ export type PeerMessage = {
18
20
  type: "peer";
19
21
  /** The PeerID of the server */
20
22
  senderId: PeerId;
23
+ /** Metadata presented by the peer */
24
+ peerMetadata: PeerMetadata;
21
25
  /** The protocol version the server selected for this connection */
22
26
  selectedProtocolVersion: ProtocolVersion;
23
27
  /** The PeerID of the client */
@@ -37,4 +41,8 @@ export type ErrorMessage = {
37
41
  export type FromClientMessage = JoinMessage | LeaveMessage | Message;
38
42
  /** A message from the server to the client */
39
43
  export type FromServerMessage = PeerMessage | ErrorMessage | Message;
44
+ export declare const isJoinMessage: (message: FromClientMessage) => message is JoinMessage;
45
+ export declare const isLeaveMessage: (message: FromClientMessage) => message is LeaveMessage;
46
+ export declare const isPeerMessage: (message: FromServerMessage) => message is PeerMessage;
47
+ export declare const isErrorMessage: (message: FromServerMessage) => message is ErrorMessage;
40
48
  //# sourceMappingURL=messages.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAA;AAChE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAE3D,kCAAkC;AAClC,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,OAAO,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,6EAA6E;AAC7E,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAChB,+CAA+C;IAC/C,yBAAyB,EAAE,eAAe,EAAE,CAAA;CAC7C,CAAA;AAED,yFAAyF;AACzF,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAChB,mEAAmE;IACnE,uBAAuB,EAAE,eAAe,CAAA;IACxC,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,gGAAgG;AAChG,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,OAAO,CAAA;IACb,mCAAmC;IACnC,QAAQ,EAAE,MAAM,CAAA;IAChB,gCAAgC;IAChC,OAAO,EAAE,MAAM,CAAA;IACf,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAKD,8CAA8C;AAC9C,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,YAAY,GAAG,OAAO,CAAA;AAEpE,8CAA8C;AAC9C,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,YAAY,GAAG,OAAO,CAAA"}
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAC9E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAE3D,kCAAkC;AAClC,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,OAAO,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,6EAA6E;AAC7E,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAEhB,sCAAsC;IACtC,YAAY,EAAE,YAAY,CAAA;IAE1B,+CAA+C;IAC/C,yBAAyB,EAAE,eAAe,EAAE,CAAA;CAC7C,CAAA;AAED,yFAAyF;AACzF,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAEhB,sCAAsC;IACtC,YAAY,EAAE,YAAY,CAAA;IAE1B,mEAAmE;IACnE,uBAAuB,EAAE,eAAe,CAAA;IACxC,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,gGAAgG;AAChG,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,OAAO,CAAA;IACb,mCAAmC;IACnC,QAAQ,EAAE,MAAM,CAAA;IAChB,gCAAgC;IAChC,OAAO,EAAE,MAAM,CAAA;IACf,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,8CAA8C;AAC9C,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,YAAY,GAAG,OAAO,CAAA;AAEpE,8CAA8C;AAC9C,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,YAAY,GAAG,OAAO,CAAA;AAIpE,eAAO,MAAM,aAAa,YACf,iBAAiB,2BACwB,CAAA;AAEpD,eAAO,MAAM,cAAc,YAChB,iBAAiB,4BAC0B,CAAA;AAEtD,eAAO,MAAM,aAAa,YACf,iBAAiB,2BACwB,CAAA;AAEpD,eAAO,MAAM,cAAc,YAChB,iBAAiB,4BAC0B,CAAA"}
package/dist/messages.js CHANGED
@@ -1 +1,5 @@
1
- export {};
1
+ // TYPE GUARDS
2
+ export const isJoinMessage = (message) => message.type === "join";
3
+ export const isLeaveMessage = (message) => message.type === "leave";
4
+ export const isPeerMessage = (message) => message.type === "peer";
5
+ export const isErrorMessage = (message) => message.type === "error";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * This incantation deals with websocket sending the whole underlying buffer even if we just have a
3
+ * uint8array view on it
4
+ */
5
+ export declare const toArrayBuffer: (bytes: Uint8Array) => ArrayBuffer;
6
+ //# sourceMappingURL=toArrayBuffer.d.ts.map