@automerge/automerge-repo-network-websocket 1.1.0-alpha.7 → 1.1.1

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.
@@ -2,30 +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
19
  connect(peerId, peerMetadata) {
15
20
  this.peerId = peerId;
16
21
  this.peerMetadata = peerMetadata;
17
- this.server.on("close", function close() {
18
- clearInterval(interval);
22
+ this.server.on("close", () => {
23
+ clearInterval(keepAliveId);
24
+ this.disconnect();
19
25
  });
20
26
  this.server.on("connection", (socket) => {
21
27
  // When a socket closes, or disconnects, remove it from our list
22
28
  socket.on("close", () => {
23
- for (const [otherPeerId, otherSocket] of Object.entries(this.sockets)) {
24
- if (socket === otherSocket) {
25
- this.emit("peer-disconnected", { peerId: otherPeerId });
26
- delete this.sockets[otherPeerId];
27
- }
28
- }
29
+ this.#removeSocket(socket);
29
30
  });
30
31
  socket.on("message", message => this.receiveMessage(message, socket));
31
32
  // Start out "alive", and every time we get a pong, reset that state.
@@ -33,115 +34,117 @@ export class NodeWSServerAdapter extends NetworkAdapter {
33
34
  socket.on("pong", () => (socket.isAlive = true));
34
35
  this.emit("ready", { network: this });
35
36
  });
36
- // Every interval, terminate connections to lost clients,
37
- // then mark all clients as potentially dead and then ping them.
38
- const interval = setInterval(() => {
39
- ;
40
- this.server.clients.forEach(socket => {
41
- if (socket.isAlive === false) {
42
- // Make sure we clean up this socket even though we're terminating.
43
- // This might be unnecessary but I have read reports of the close() not happening for 30s.
44
- for (const [otherPeerId, otherSocket] of Object.entries(this.sockets)) {
45
- if (socket === otherSocket) {
46
- this.emit("peer-disconnected", { peerId: otherPeerId });
47
- delete this.sockets[otherPeerId];
48
- }
49
- }
50
- 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);
51
48
  }
52
- socket.isAlive = false;
53
- socket.ping();
54
49
  });
55
- }, 5000);
50
+ }, this.keepAliveInterval);
56
51
  }
57
52
  disconnect() {
58
- // 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
+ });
59
58
  }
60
59
  send(message) {
61
- if ("data" in message && message.data.byteLength === 0) {
62
- throw new Error("tried to send a zero-length message");
63
- }
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");
64
63
  const senderId = this.peerId;
65
- if (!senderId) {
66
- throw new Error("No peerId set for the websocket server network adapter.");
67
- }
68
- if (this.sockets[message.targetId] === undefined) {
69
- 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}`);
70
68
  return;
71
69
  }
72
70
  const encoded = encode(message);
73
- // This incantation deals with websocket sending the whole
74
- // underlying buffer even if we just have a uint8array view on it
75
- const arrayBuf = encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength);
76
- this.sockets[message.targetId]?.send(arrayBuf);
71
+ const arrayBuf = toArrayBuffer(encoded);
72
+ socket.send(arrayBuf);
77
73
  }
78
- receiveMessage(message, socket) {
79
- const cbor = decode(message);
80
- const { type, senderId } = cbor;
74
+ receiveMessage(messageBytes, socket) {
75
+ const message = decode(messageBytes);
76
+ const { type, senderId } = message;
81
77
  const myPeerId = this.peerId;
82
- if (!myPeerId) {
83
- throw new Error("Missing my peer ID.");
84
- }
85
- log(`[${senderId}->${myPeerId}${"documentId" in cbor ? "@" + cbor.documentId : ""}] ${type} | ${message.byteLength} bytes`);
86
- switch (type) {
87
- case "join":
88
- {
89
- const existingSocket = this.sockets[senderId];
90
- if (existingSocket) {
91
- if (existingSocket.readyState === WebSocket.OPEN) {
92
- existingSocket.close();
93
- }
94
- this.emit("peer-disconnected", { peerId: senderId });
95
- }
96
- const { peerMetadata } = cbor;
97
- // Let the rest of the system know that we have a new connection.
98
- this.emit("peer-candidate", {
99
- peerId: senderId,
100
- peerMetadata,
101
- });
102
- this.sockets[senderId] = socket;
103
- // In this client-server connection, there's only ever one peer: us!
104
- // (and we pretend to be joined to every channel)
105
- const selectedProtocolVersion = selectProtocol(cbor.supportedProtocolVersions);
106
- if (selectedProtocolVersion === null) {
107
- this.send({
108
- type: "error",
109
- senderId: this.peerId,
110
- message: "unsupported protocol version",
111
- targetId: senderId,
112
- });
113
- this.sockets[senderId].close();
114
- delete this.sockets[senderId];
115
- }
116
- else {
117
- this.send({
118
- type: "peer",
119
- senderId: this.peerId,
120
- peerMetadata: this.peerMetadata,
121
- selectedProtocolVersion: ProtocolV1,
122
- targetId: senderId,
123
- });
124
- }
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();
125
88
  }
126
- break;
127
- case "leave":
128
- // It doesn't seem like this gets called;
129
- // we handle leaving in the socket close logic
130
- // TODO: confirm this
131
- // ?
132
- break;
133
- default:
134
- this.emit("message", cbor);
135
- 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);
136
122
  }
123
+ else {
124
+ this.emit("message", message);
125
+ }
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
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
+ };
138
143
  }
139
- function selectProtocol(versions) {
140
- if (versions === undefined) {
144
+ const selectProtocol = (versions) => {
145
+ if (versions === undefined)
141
146
  return ProtocolV1;
142
- }
143
- if (versions.includes(ProtocolV1)) {
147
+ if (versions.includes(ProtocolV1))
144
148
  return ProtocolV1;
145
- }
146
149
  return null;
147
- }
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 */
@@ -41,4 +41,8 @@ export type ErrorMessage = {
41
41
  export type FromClientMessage = JoinMessage | LeaveMessage | Message;
42
42
  /** A message from the server to the client */
43
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;
44
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,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;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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toArrayBuffer.d.ts","sourceRoot":"","sources":["../src/toArrayBuffer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,aAAa,UAAW,UAAU,gBAG9C,CAAA"}
@@ -0,0 +1,8 @@
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 const toArrayBuffer = (bytes) => {
6
+ const { buffer, byteOffset, byteLength } = bytes;
7
+ return buffer.slice(byteOffset, byteOffset + byteLength);
8
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automerge/automerge-repo-network-websocket",
3
- "version": "1.1.0-alpha.7",
3
+ "version": "1.1.1",
4
4
  "description": "isomorphic node/browser Websocket network adapter for Automerge Repo",
5
5
  "repository": "https://github.com/automerge/automerge-repo/tree/master/packages/automerge-repo-network-websocket",
6
6
  "author": "Peter van Hardenberg <pvh@pvh.ca>",
@@ -13,8 +13,9 @@
13
13
  "test": "vitest"
14
14
  },
15
15
  "dependencies": {
16
- "@automerge/automerge-repo": "^1.1.0-alpha.7",
16
+ "@automerge/automerge-repo": "1.1.1",
17
17
  "cbor-x": "^1.3.0",
18
+ "debug": "^4.3.4",
18
19
  "eventemitter3": "^5.0.1",
19
20
  "isomorphic-ws": "^5.0.0",
20
21
  "ws": "^8.7.0"
@@ -30,5 +31,5 @@
30
31
  "publishConfig": {
31
32
  "access": "public"
32
33
  },
33
- "gitHead": "9a4711e39c93273d992c5686257246ddfaaafddd"
34
+ "gitHead": "7e0681014b8c5f672e2abc2a653a954ccb6d7aba"
34
35
  }
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  NetworkAdapter,
3
- type PeerId,
4
- type PeerMetadata,
3
+ PeerId,
4
+ PeerMetadata,
5
5
  cbor,
6
6
  } from "@automerge/automerge-repo"
7
7
  import WebSocket from "isomorphic-ws"
@@ -12,173 +12,163 @@ import {
12
12
  FromClientMessage,
13
13
  FromServerMessage,
14
14
  JoinMessage,
15
- PeerMessage,
15
+ isErrorMessage,
16
+ isPeerMessage,
16
17
  } from "./messages.js"
17
18
  import { ProtocolV1 } from "./protocolVersion.js"
18
-
19
- const log = debug("WebsocketClient")
19
+ import { assert } from "./assert.js"
20
+ import { toArrayBuffer } from "./toArrayBuffer.js"
20
21
 
21
22
  abstract class WebSocketNetworkAdapter extends NetworkAdapter {
22
23
  socket?: WebSocket
23
24
  }
24
25
 
25
26
  export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
26
- // Type trickery required for platform independence,
27
- // see https://stackoverflow.com/questions/45802988/typescript-use-correct-version-of-settimeout-node-vs-window
28
- timerId?: ReturnType<typeof setTimeout>
29
- remotePeerId?: PeerId // this adapter only connects to one remote client at a time
30
- #startupComplete: boolean = false
27
+ #isReady = false
28
+ #retryIntervalId?: TimeoutId
29
+ #log = debug("automerge-repo:websocket:browser")
31
30
 
32
- url: string
31
+ remotePeerId?: PeerId // this adapter only connects to one remote client at a time
33
32
 
34
- constructor(url: string) {
33
+ constructor(
34
+ public readonly url: string,
35
+ public readonly retryInterval = 5000
36
+ ) {
35
37
  super()
36
- this.url = url
38
+ this.#log = this.#log.extend(url)
37
39
  }
38
40
 
39
- connect(peerId: PeerId, peerMetadata: PeerMetadata) {
40
- // If we're reconnecting make sure we remove the old event listeners
41
- // before creating a new connection.
42
- if (this.socket) {
41
+ connect(peerId: PeerId, peerMetadata?: PeerMetadata) {
42
+ if (!this.socket || !this.peerId) {
43
+ // first time connecting
44
+ this.#log("connecting")
45
+ this.peerId = peerId
46
+ this.peerMetadata = peerMetadata ?? {}
47
+ } else {
48
+ this.#log("reconnecting")
49
+ assert(peerId === this.peerId)
50
+ // Remove the old event listeners before creating a new connection.
43
51
  this.socket.removeEventListener("open", this.onOpen)
44
52
  this.socket.removeEventListener("close", this.onClose)
45
53
  this.socket.removeEventListener("message", this.onMessage)
54
+ this.socket.removeEventListener("error", this.onError)
46
55
  }
56
+ // Wire up retries
57
+ if (!this.#retryIntervalId)
58
+ this.#retryIntervalId = setInterval(() => {
59
+ this.connect(peerId, peerMetadata)
60
+ }, this.retryInterval)
47
61
 
48
- if (!this.timerId) {
49
- this.timerId = setInterval(() => this.connect(peerId, peerMetadata), 5000)
50
- }
51
-
52
- this.peerId = peerId
53
- this.peerMetadata = peerMetadata
54
62
  this.socket = new WebSocket(this.url)
63
+
55
64
  this.socket.binaryType = "arraybuffer"
56
65
 
57
66
  this.socket.addEventListener("open", this.onOpen)
58
67
  this.socket.addEventListener("close", this.onClose)
59
68
  this.socket.addEventListener("message", this.onMessage)
69
+ this.socket.addEventListener("error", this.onError)
60
70
 
61
- // mark this adapter as ready if we haven't received an ack in 1 second.
71
+ // Mark this adapter as ready if we haven't received an ack in 1 second.
62
72
  // We might hear back from the other end at some point but we shouldn't
63
73
  // hold up marking things as unavailable for any longer
64
- setTimeout(() => {
65
- if (!this.#startupComplete) {
66
- this.#startupComplete = true
67
- this.emit("ready", { network: this })
68
- }
69
- }, 1000)
70
-
74
+ setTimeout(() => this.#ready(), 1000)
71
75
  this.join()
72
76
  }
73
77
 
74
78
  onOpen = () => {
75
- log(`@ ${this.url}: open`)
76
- clearInterval(this.timerId)
77
- this.timerId = undefined
78
- this.send(joinMessage(this.peerId!, this.peerMetadata!))
79
+ this.#log("open")
80
+ clearInterval(this.#retryIntervalId)
81
+ this.#retryIntervalId = undefined
82
+ this.join()
79
83
  }
80
84
 
81
85
  // When a socket closes, or disconnects, remove it from the array.
82
86
  onClose = () => {
83
- log(`${this.url}: close`)
84
-
85
- if (this.remotePeerId) {
87
+ this.#log("close")
88
+ if (this.remotePeerId)
86
89
  this.emit("peer-disconnected", { peerId: this.remotePeerId })
87
- }
88
90
 
89
- if (!this.timerId) {
90
- if (this.peerId) {
91
- this.connect(this.peerId, this.peerMetadata!)
92
- }
93
- }
91
+ if (this.retryInterval > 0 && !this.#retryIntervalId)
92
+ // try to reconnect
93
+ setTimeout(() => {
94
+ assert(this.peerId)
95
+ return this.connect(this.peerId, this.peerMetadata)
96
+ }, this.retryInterval)
94
97
  }
95
98
 
96
99
  onMessage = (event: WebSocket.MessageEvent) => {
97
100
  this.receiveMessage(event.data as Uint8Array)
98
101
  }
99
102
 
100
- join() {
101
- if (!this.socket) {
102
- throw new Error("WTF, get a socket")
103
+ onError = (event: WebSocket.ErrorEvent) => {
104
+ const { code } = event.error
105
+ if (code === "ECONNREFUSED") {
106
+ this.#log("Connection refused, retrying...")
107
+ } else {
108
+ /* c8 ignore next */
109
+ throw event.error
103
110
  }
111
+ }
112
+
113
+ #ready() {
114
+ if (this.#isReady) return
115
+ this.#isReady = true
116
+ this.emit("ready", { network: this })
117
+ }
118
+
119
+ join() {
120
+ assert(this.peerId)
121
+ assert(this.socket)
104
122
  if (this.socket.readyState === WebSocket.OPEN) {
105
123
  this.send(joinMessage(this.peerId!, this.peerMetadata!))
106
124
  } else {
107
- // The onOpen handler automatically sends a join message
125
+ // We'll try again in the `onOpen` handler
108
126
  }
109
127
  }
110
128
 
111
129
  disconnect() {
112
- if (!this.socket) {
113
- throw new Error("WTF, get a socket")
114
- }
115
- this.send({ type: "leave", senderId: this.peerId! })
130
+ assert(this.peerId)
131
+ assert(this.socket)
132
+ this.send({ type: "leave", senderId: this.peerId })
116
133
  }
117
134
 
118
135
  send(message: FromClientMessage) {
119
- if ("data" in message && message.data.byteLength === 0) {
120
- throw new Error("tried to send a zero-length message")
121
- }
122
-
123
- if (!this.peerId) {
124
- throw new Error("Why don't we have a PeerID?")
125
- }
126
-
127
- if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
128
- throw new Error("Websocket Socket not ready!")
129
- }
136
+ if ("data" in message && message.data?.byteLength === 0)
137
+ throw new Error("Tried to send a zero-length message")
138
+ assert(this.peerId)
139
+ assert(this.socket)
140
+ if (this.socket.readyState !== WebSocket.OPEN)
141
+ throw new Error(`Websocket not ready (${this.socket.readyState})`)
130
142
 
131
143
  const encoded = cbor.encode(message)
132
- // This incantation deals with websocket sending the whole
133
- // underlying buffer even if we just have a uint8array view on it
134
- const arrayBuf = encoded.buffer.slice(
135
- encoded.byteOffset,
136
- encoded.byteOffset + encoded.byteLength
137
- )
138
-
139
- this.socket?.send(arrayBuf)
144
+ this.socket.send(toArrayBuffer(encoded))
140
145
  }
141
146
 
142
- announceConnection(peerId: PeerId, peerMetadata: PeerMetadata) {
143
- // return a peer object
144
- const myPeerId = this.peerId
145
- if (!myPeerId) {
146
- throw new Error("we should have a peer ID by now")
147
- }
148
- if (!this.#startupComplete) {
149
- this.#startupComplete = true
150
- this.emit("ready", { network: this })
151
- }
152
- this.remotePeerId = peerId
153
- this.emit("peer-candidate", { peerId, peerMetadata })
147
+ peerCandidate(remotePeerId: PeerId, peerMetadata: PeerMetadata) {
148
+ assert(this.socket)
149
+ this.#ready()
150
+ this.remotePeerId = remotePeerId
151
+ this.emit("peer-candidate", {
152
+ peerId: remotePeerId,
153
+ peerMetadata,
154
+ })
154
155
  }
155
156
 
156
- receiveMessage(message: Uint8Array) {
157
- const decoded: FromServerMessage = cbor.decode(new Uint8Array(message))
158
-
159
- const { type, senderId } = decoded
160
-
161
- const socket = this.socket
162
- if (!socket) {
163
- throw new Error("Missing socket at receiveMessage")
164
- }
157
+ receiveMessage(messageBytes: Uint8Array) {
158
+ const message: FromServerMessage = cbor.decode(new Uint8Array(messageBytes))
165
159
 
166
- if (message.byteLength === 0) {
160
+ assert(this.socket)
161
+ if (messageBytes.byteLength === 0)
167
162
  throw new Error("received a zero-length message")
168
- }
169
163
 
170
- switch (type) {
171
- case "peer": {
172
- const { peerMetadata } = decoded
173
- log(`peer: ${senderId}`)
174
- this.announceConnection(senderId, peerMetadata)
175
- break
176
- }
177
- case "error":
178
- log(`error: ${decoded.message}`)
179
- break
180
- default:
181
- this.emit("message", decoded)
164
+ if (isPeerMessage(message)) {
165
+ const { peerMetadata } = message
166
+ this.#log(`peer: ${message.senderId}`)
167
+ this.peerCandidate(message.senderId, peerMetadata)
168
+ } else if (isErrorMessage(message)) {
169
+ this.#log(`error: ${message.message}`)
170
+ } else {
171
+ this.emit("message", message)
182
172
  }
183
173
  }
184
174
  }
@@ -194,3 +184,5 @@ function joinMessage(
194
184
  supportedProtocolVersions: [ProtocolV1],
195
185
  }
196
186
  }
187
+
188
+ type TimeoutId = ReturnType<typeof setTimeout> // https://stackoverflow.com/questions/45802988