@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.
- package/README.md +77 -3
- package/dist/BrowserWebSocketClientAdapter.d.ts +8 -7
- package/dist/BrowserWebSocketClientAdapter.d.ts.map +1 -1
- package/dist/BrowserWebSocketClientAdapter.js +91 -86
- package/dist/NodeWSServerAdapter.d.ts +6 -4
- package/dist/NodeWSServerAdapter.d.ts.map +1 -1
- package/dist/NodeWSServerAdapter.js +105 -102
- package/dist/assert.d.ts +3 -0
- package/dist/assert.d.ts.map +1 -0
- package/dist/assert.js +17 -0
- package/dist/messages.d.ts +4 -0
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +5 -1
- package/dist/toArrayBuffer.d.ts +6 -0
- package/dist/toArrayBuffer.d.ts.map +1 -0
- package/dist/toArrayBuffer.js +8 -0
- package/package.json +4 -3
- package/src/BrowserWebSocketClientAdapter.ts +98 -106
- package/src/NodeWSServerAdapter.ts +119 -123
- package/src/assert.ts +28 -0
- package/src/messages.ts +18 -3
- package/src/toArrayBuffer.ts +8 -0
- package/test/Websocket.test.ts +350 -125
|
@@ -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",
|
|
18
|
-
clearInterval(
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
},
|
|
50
|
+
}, this.keepAliveInterval);
|
|
56
51
|
}
|
|
57
52
|
disconnect() {
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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(
|
|
79
|
-
const
|
|
80
|
-
const { type, senderId } =
|
|
74
|
+
receiveMessage(messageBytes, socket) {
|
|
75
|
+
const message = decode(messageBytes);
|
|
76
|
+
const { type, senderId } = message;
|
|
81
77
|
const myPeerId = this.peerId;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
log(`[${senderId}->${myPeerId}${
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
+
};
|
package/dist/assert.d.ts
ADDED
|
@@ -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 */
|
package/dist/messages.d.ts
CHANGED
|
@@ -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
|
package/dist/messages.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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 @@
|
|
|
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.
|
|
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": "
|
|
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": "
|
|
34
|
+
"gitHead": "7e0681014b8c5f672e2abc2a653a954ccb6d7aba"
|
|
34
35
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
NetworkAdapter,
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
15
|
+
isErrorMessage,
|
|
16
|
+
isPeerMessage,
|
|
16
17
|
} from "./messages.js"
|
|
17
18
|
import { ProtocolV1 } from "./protocolVersion.js"
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
31
|
+
remotePeerId?: PeerId // this adapter only connects to one remote client at a time
|
|
33
32
|
|
|
34
|
-
constructor(
|
|
33
|
+
constructor(
|
|
34
|
+
public readonly url: string,
|
|
35
|
+
public readonly retryInterval = 5000
|
|
36
|
+
) {
|
|
35
37
|
super()
|
|
36
|
-
this
|
|
38
|
+
this.#log = this.#log.extend(url)
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
connect(peerId: PeerId, peerMetadata
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
//
|
|
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(
|
|
76
|
-
clearInterval(this
|
|
77
|
-
this
|
|
78
|
-
this.
|
|
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(
|
|
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
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
//
|
|
125
|
+
// We'll try again in the `onOpen` handler
|
|
108
126
|
}
|
|
109
127
|
}
|
|
110
128
|
|
|
111
129
|
disconnect() {
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
120
|
-
throw new Error("
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
throw new Error(
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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(
|
|
157
|
-
const
|
|
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
|
-
|
|
160
|
+
assert(this.socket)
|
|
161
|
+
if (messageBytes.byteLength === 0)
|
|
167
162
|
throw new Error("received a zero-length message")
|
|
168
|
-
}
|
|
169
163
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|