@automerge/automerge-repo-network-websocket 0.0.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/.mocharc.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "extension": ["ts"],
3
+ "spec": "test/*.test.ts",
4
+ "loader": "ts-node/esm"
5
+ }
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # Automerge-Repo Network: Websocket
2
+
3
+ Includes two implementations, a Websocket client and a Websocket server. These are used by the example sync-server.
4
+
5
+ The package uses isomorphic-ws to share code between node and the browser, but the server code is node only due to lack of browser support.
@@ -0,0 +1,21 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="ws" />
3
+ import { ChannelId, NetworkAdapter, PeerId } from "@automerge/automerge-repo";
4
+ import WebSocket from "isomorphic-ws";
5
+ declare abstract class WebSocketNetworkAdapter extends NetworkAdapter {
6
+ socket?: WebSocket;
7
+ }
8
+ export declare class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
9
+ timerId?: NodeJS.Timer;
10
+ url: string;
11
+ channels: ChannelId[];
12
+ constructor(url: string);
13
+ connect(peerId: PeerId): void;
14
+ join(channelId: ChannelId): void;
15
+ leave(channelId: ChannelId): void;
16
+ sendMessage(targetId: PeerId, channelId: ChannelId, message: Uint8Array, broadcast: boolean): void;
17
+ announceConnection(channelId: ChannelId, peerId: PeerId): void;
18
+ receiveMessage(message: Uint8Array): void;
19
+ }
20
+ export {};
21
+ //# sourceMappingURL=BrowserWebSocketClientAdapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BrowserWebSocketClientAdapter.d.ts","sourceRoot":"","sources":["../src/BrowserWebSocketClientAdapter.ts"],"names":[],"mappings":";;AAAA,OAAO,EACL,SAAS,EAET,cAAc,EACd,MAAM,EACP,MAAM,2BAA2B,CAAA;AAElC,OAAO,SAAS,MAAM,eAAe,CAAA;AAKrC,uBAAe,uBAAwB,SAAQ,cAAc;IAC3D,MAAM,CAAC,EAAE,SAAS,CAAA;CACnB;AAED,qBAAa,6BAA8B,SAAQ,uBAAuB;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC,KAAK,CAAA;IACtB,GAAG,EAAE,MAAM,CAAA;IACX,QAAQ,EAAE,SAAS,EAAE,CAAK;gBAEd,GAAG,EAAE,MAAM;IAKvB,OAAO,CAAC,MAAM,EAAE,MAAM;IA8BtB,IAAI,CAAC,SAAS,EAAE,SAAS;IA6BzB,KAAK,CAAC,SAAS,EAAE,SAAS;IAU1B,WAAW,CACT,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,UAAU,EACnB,SAAS,EAAE,OAAO;IAgCpB,kBAAkB,CAAC,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM;IAUvD,cAAc,CAAC,OAAO,EAAE,UAAU;CAoCnC"}
@@ -0,0 +1,124 @@
1
+ import { NetworkAdapter, } from "@automerge/automerge-repo";
2
+ import * as CBOR from "cbor-x";
3
+ import WebSocket from "isomorphic-ws";
4
+ import debug from "debug";
5
+ const log = debug("WebsocketClient");
6
+ class WebSocketNetworkAdapter extends NetworkAdapter {
7
+ socket;
8
+ }
9
+ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
10
+ timerId;
11
+ url;
12
+ channels = [];
13
+ constructor(url) {
14
+ super();
15
+ this.url = url;
16
+ }
17
+ connect(peerId) {
18
+ if (!this.timerId) {
19
+ this.timerId = setInterval(() => this.connect(peerId), 5000);
20
+ }
21
+ this.peerId = peerId;
22
+ this.socket = new WebSocket(this.url);
23
+ this.socket.binaryType = "arraybuffer";
24
+ this.socket.addEventListener("open", () => {
25
+ log(`@ ${this.url}: open`);
26
+ clearInterval(this.timerId);
27
+ this.timerId = undefined;
28
+ this.channels.forEach(c => this.join(c));
29
+ });
30
+ // When a socket closes, or disconnects, remove it from the array.
31
+ this.socket.addEventListener("close", () => {
32
+ log(`${this.url}: close`);
33
+ if (!this.timerId) {
34
+ this.connect(peerId);
35
+ }
36
+ // log("Disconnected from server")
37
+ });
38
+ this.socket.addEventListener("message", (event) => this.receiveMessage(event.data));
39
+ }
40
+ join(channelId) {
41
+ // TODO: the network subsystem should manage this
42
+ if (!this.channels.includes(channelId)) {
43
+ this.channels.push(channelId);
44
+ }
45
+ if (!this.socket) {
46
+ throw new Error("WTF, get a socket");
47
+ }
48
+ if (this.socket.readyState === WebSocket.OPEN) {
49
+ this.socket.send(CBOR.encode({ type: "join", channelId, senderId: this.peerId }));
50
+ }
51
+ else {
52
+ this.socket.addEventListener("open", () => {
53
+ if (!this.socket) {
54
+ throw new Error("WTF, get a socket");
55
+ }
56
+ this.socket.send(CBOR.encode({ type: "join", channelId, senderId: this.peerId }));
57
+ }, { once: true });
58
+ }
59
+ }
60
+ leave(channelId) {
61
+ this.channels = this.channels.filter(c => c !== channelId);
62
+ if (!this.socket) {
63
+ throw new Error("WTF, get a socket");
64
+ }
65
+ this.socket.send(CBOR.encode({ type: "leave", channelId, senderId: this.peerId }));
66
+ }
67
+ sendMessage(targetId, channelId, message, broadcast) {
68
+ if (message.byteLength === 0) {
69
+ throw new Error("tried to send a zero-length message");
70
+ }
71
+ if (!this.peerId) {
72
+ throw new Error("Why don't we have a PeerID?");
73
+ }
74
+ const decoded = {
75
+ senderId: this.peerId,
76
+ targetId,
77
+ channelId,
78
+ type: "message",
79
+ message,
80
+ broadcast,
81
+ };
82
+ const encoded = CBOR.encode(decoded);
83
+ // This incantation deals with websocket sending the whole
84
+ // underlying buffer even if we just have a uint8array view on it
85
+ const arrayBuf = encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength);
86
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
87
+ throw new Error("Websocket Socket not ready!");
88
+ }
89
+ this.socket.send(arrayBuf);
90
+ }
91
+ announceConnection(channelId, peerId) {
92
+ // return a peer object
93
+ const myPeerId = this.peerId;
94
+ if (!myPeerId) {
95
+ throw new Error("we should have a peer ID by now");
96
+ }
97
+ this.emit("peer-candidate", { peerId, channelId });
98
+ }
99
+ receiveMessage(message) {
100
+ const decoded = CBOR.decode(new Uint8Array(message));
101
+ const { type, senderId, targetId, channelId, message: messageData, broadcast, } = decoded;
102
+ const socket = this.socket;
103
+ if (!socket) {
104
+ throw new Error("Missing socket at receiveMessage");
105
+ }
106
+ if (message.byteLength === 0) {
107
+ throw new Error("received a zero-length message");
108
+ }
109
+ switch (type) {
110
+ case "peer":
111
+ log(`peer: ${senderId}, ${channelId}`);
112
+ this.announceConnection(channelId, senderId);
113
+ break;
114
+ default:
115
+ this.emit("message", {
116
+ channelId,
117
+ senderId,
118
+ targetId,
119
+ message: new Uint8Array(messageData),
120
+ broadcast,
121
+ });
122
+ }
123
+ }
124
+ }
@@ -0,0 +1,16 @@
1
+ /// <reference types="ws" />
2
+ import { WebSocket, type WebSocketServer } from "isomorphic-ws";
3
+ import { ChannelId, NetworkAdapter, PeerId } from "@automerge/automerge-repo";
4
+ export declare class NodeWSServerAdapter extends NetworkAdapter {
5
+ server: WebSocketServer;
6
+ sockets: {
7
+ [peerId: PeerId]: WebSocket;
8
+ };
9
+ constructor(server: WebSocketServer);
10
+ connect(peerId: PeerId): void;
11
+ join(docId: ChannelId): void;
12
+ leave(docId: ChannelId): void;
13
+ sendMessage(targetId: PeerId, channelId: ChannelId, message: Uint8Array, broadcast: boolean): void;
14
+ receiveMessage(message: Uint8Array, socket: WebSocket): void;
15
+ }
16
+ //# sourceMappingURL=NodeWSServerAdapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NodeWSServerAdapter.d.ts","sourceRoot":"","sources":["../src/NodeWSServerAdapter.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAA;AAK/D,OAAO,EACL,SAAS,EAET,cAAc,EACd,MAAM,EACP,MAAM,2BAA2B,CAAA;AAElC,qBAAa,mBAAoB,SAAQ,cAAc;IACrD,MAAM,EAAE,eAAe,CAAA;IACvB,OAAO,EAAE;QAAE,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAK;gBAEjC,MAAM,EAAE,eAAe;IAKnC,OAAO,CAAC,MAAM,EAAE,MAAM;IAmBtB,IAAI,CAAC,KAAK,EAAE,SAAS;IAIrB,KAAK,CAAC,KAAK,EAAE,SAAS;IAItB,WAAW,CACT,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,UAAU,EACnB,SAAS,EAAE,OAAO;IAsCpB,cAAc,CAAC,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS;CAmDtD"}
@@ -0,0 +1,97 @@
1
+ import * as CBOR from "cbor-x";
2
+ import debug from "debug";
3
+ const log = debug("WebsocketServer");
4
+ import { NetworkAdapter, } from "@automerge/automerge-repo";
5
+ export class NodeWSServerAdapter extends NetworkAdapter {
6
+ server;
7
+ sockets = {};
8
+ constructor(server) {
9
+ super();
10
+ this.server = server;
11
+ }
12
+ connect(peerId) {
13
+ this.peerId = peerId;
14
+ this.server.on("connection", socket => {
15
+ // When a socket closes, or disconnects, remove it from our list
16
+ socket.on("close", () => {
17
+ for (const [otherPeerId, otherSocket] of Object.entries(this.sockets)) {
18
+ if (socket === otherSocket) {
19
+ this.emit("peer-disconnected", { peerId: otherPeerId });
20
+ delete this.sockets[otherPeerId];
21
+ }
22
+ }
23
+ });
24
+ socket.on("message", message => this.receiveMessage(message, socket));
25
+ });
26
+ }
27
+ join(docId) {
28
+ // throw new Error("The server doesn't join channels.")
29
+ }
30
+ leave(docId) {
31
+ // throw new Error("The server doesn't join channels.")
32
+ }
33
+ sendMessage(targetId, channelId, message, broadcast) {
34
+ if (message.byteLength === 0) {
35
+ throw new Error("tried to send a zero-length message");
36
+ }
37
+ const senderId = this.peerId;
38
+ if (!senderId) {
39
+ throw new Error("No peerId set for the websocket server network adapter.");
40
+ }
41
+ if (this.sockets[targetId] === undefined) {
42
+ log(`Tried to send message to disconnected peer: ${targetId}`);
43
+ return;
44
+ }
45
+ const decoded = {
46
+ senderId,
47
+ targetId,
48
+ channelId,
49
+ type: "sync",
50
+ message,
51
+ broadcast,
52
+ };
53
+ const encoded = CBOR.encode(decoded);
54
+ // This incantation deals with websocket sending the whole
55
+ // underlying buffer even if we just have a uint8array view on it
56
+ const arrayBuf = encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength);
57
+ log(`[${senderId}->${targetId}@${channelId}] "sync" | ${arrayBuf.byteLength} bytes`);
58
+ this.sockets[targetId].send(arrayBuf);
59
+ }
60
+ receiveMessage(message, socket) {
61
+ const cbor = CBOR.decode(message);
62
+ const { type, channelId, senderId, targetId, message: data, broadcast, } = cbor;
63
+ const myPeerId = this.peerId;
64
+ if (!myPeerId) {
65
+ throw new Error("Missing my peer ID.");
66
+ }
67
+ log(`[${senderId}->${myPeerId}@${channelId}] ${type} | ${message.byteLength} bytes`);
68
+ switch (type) {
69
+ case "join":
70
+ // Let the rest of the system know that we have a new connection.
71
+ this.emit("peer-candidate", { peerId: senderId, channelId });
72
+ this.sockets[senderId] = socket;
73
+ // In this client-server connection, there's only ever one peer: us!
74
+ // (and we pretend to be joined to every channel)
75
+ socket.send(CBOR.encode({ type: "peer", senderId: this.peerId, channelId }));
76
+ break;
77
+ case "leave":
78
+ // It doesn't seem like this gets called;
79
+ // we handle leaving in the socket close logic
80
+ // TODO: confirm this
81
+ // ?
82
+ break;
83
+ case "message":
84
+ this.emit("message", {
85
+ senderId,
86
+ targetId,
87
+ channelId,
88
+ message: new Uint8Array(data),
89
+ broadcast,
90
+ });
91
+ break;
92
+ default:
93
+ // log("unrecognized message type")
94
+ break;
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,3 @@
1
+ export { BrowserWebSocketClientAdapter } from "./BrowserWebSocketClientAdapter.js";
2
+ export { NodeWSServerAdapter } from "./NodeWSServerAdapter.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,6BAA6B,EAAE,MAAM,oCAAoC,CAAA;AAClF,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { BrowserWebSocketClientAdapter } from "./BrowserWebSocketClientAdapter.js";
2
+ export { NodeWSServerAdapter } from "./NodeWSServerAdapter.js";
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@automerge/automerge-repo-network-websocket",
3
+ "version": "0.0.1",
4
+ "description": "isomorphic node/browser Websocket network adapter for Automerge Repo",
5
+ "repository": "https://github.com/pvh/automerge-repo",
6
+ "author": "Peter van Hardenberg <pvh@pvh.ca>",
7
+ "license": "MIT",
8
+ "private": false,
9
+ "type": "module",
10
+ "main": "dist/index.js",
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "watch": "npm-watch",
14
+ "test": "mocha --no-warnings --experimental-specifier-resolution=node --exit"
15
+ },
16
+ "dependencies": {
17
+ "@automerge/automerge-repo": "^0.0.1",
18
+ "cbor-x": "^1.3.0",
19
+ "eventemitter3": "^4.0.7",
20
+ "isomorphic-ws": "^5.0.0"
21
+ },
22
+ "watch": {
23
+ "build": {
24
+ "patterns": "./src/**/*",
25
+ "extensions": [
26
+ ".ts"
27
+ ]
28
+ }
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "gitHead": "e572f26ae416140b025c3ba557d9f781abbdada1"
34
+ }
@@ -0,0 +1,178 @@
1
+ import {
2
+ ChannelId,
3
+ InboundMessagePayload,
4
+ NetworkAdapter,
5
+ PeerId,
6
+ } from "@automerge/automerge-repo"
7
+ import * as CBOR from "cbor-x"
8
+ import WebSocket from "isomorphic-ws"
9
+
10
+ import debug from "debug"
11
+ const log = debug("WebsocketClient")
12
+
13
+ abstract class WebSocketNetworkAdapter extends NetworkAdapter {
14
+ socket?: WebSocket
15
+ }
16
+
17
+ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
18
+ timerId?: NodeJS.Timer
19
+ url: string
20
+ channels: ChannelId[] = []
21
+
22
+ constructor(url: string) {
23
+ super()
24
+ this.url = url
25
+ }
26
+
27
+ connect(peerId: PeerId) {
28
+ if (!this.timerId) {
29
+ this.timerId = setInterval(() => this.connect(peerId), 5000)
30
+ }
31
+
32
+ this.peerId = peerId
33
+ this.socket = new WebSocket(this.url)
34
+ this.socket.binaryType = "arraybuffer"
35
+
36
+ this.socket.addEventListener("open", () => {
37
+ log(`@ ${this.url}: open`)
38
+ clearInterval(this.timerId)
39
+ this.timerId = undefined
40
+ this.channels.forEach(c => this.join(c))
41
+ })
42
+
43
+ // When a socket closes, or disconnects, remove it from the array.
44
+ this.socket.addEventListener("close", () => {
45
+ log(`${this.url}: close`)
46
+ if (!this.timerId) {
47
+ this.connect(peerId)
48
+ }
49
+ // log("Disconnected from server")
50
+ })
51
+
52
+ this.socket.addEventListener("message", (event: WebSocket.MessageEvent) =>
53
+ this.receiveMessage(event.data as Uint8Array)
54
+ )
55
+ }
56
+
57
+ join(channelId: ChannelId) {
58
+ // TODO: the network subsystem should manage this
59
+ if (!this.channels.includes(channelId)) {
60
+ this.channels.push(channelId)
61
+ }
62
+
63
+ if (!this.socket) {
64
+ throw new Error("WTF, get a socket")
65
+ }
66
+ if (this.socket.readyState === WebSocket.OPEN) {
67
+ this.socket.send(
68
+ CBOR.encode({ type: "join", channelId, senderId: this.peerId })
69
+ )
70
+ } else {
71
+ this.socket.addEventListener(
72
+ "open",
73
+ () => {
74
+ if (!this.socket) {
75
+ throw new Error("WTF, get a socket")
76
+ }
77
+ this.socket.send(
78
+ CBOR.encode({ type: "join", channelId, senderId: this.peerId })
79
+ )
80
+ },
81
+ { once: true }
82
+ )
83
+ }
84
+ }
85
+
86
+ leave(channelId: ChannelId) {
87
+ this.channels = this.channels.filter(c => c !== channelId)
88
+ if (!this.socket) {
89
+ throw new Error("WTF, get a socket")
90
+ }
91
+ this.socket.send(
92
+ CBOR.encode({ type: "leave", channelId, senderId: this.peerId })
93
+ )
94
+ }
95
+
96
+ sendMessage(
97
+ targetId: PeerId,
98
+ channelId: ChannelId,
99
+ message: Uint8Array,
100
+ broadcast: boolean
101
+ ) {
102
+ if (message.byteLength === 0) {
103
+ throw new Error("tried to send a zero-length message")
104
+ }
105
+ if (!this.peerId) {
106
+ throw new Error("Why don't we have a PeerID?")
107
+ }
108
+
109
+ const decoded: InboundMessagePayload = {
110
+ senderId: this.peerId,
111
+ targetId,
112
+ channelId,
113
+ type: "message",
114
+ message,
115
+ broadcast,
116
+ }
117
+
118
+ const encoded = CBOR.encode(decoded)
119
+
120
+ // This incantation deals with websocket sending the whole
121
+ // underlying buffer even if we just have a uint8array view on it
122
+ const arrayBuf = encoded.buffer.slice(
123
+ encoded.byteOffset,
124
+ encoded.byteOffset + encoded.byteLength
125
+ )
126
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
127
+ throw new Error("Websocket Socket not ready!")
128
+ }
129
+ this.socket.send(arrayBuf)
130
+ }
131
+
132
+ announceConnection(channelId: ChannelId, peerId: PeerId) {
133
+ // return a peer object
134
+ const myPeerId = this.peerId
135
+ if (!myPeerId) {
136
+ throw new Error("we should have a peer ID by now")
137
+ }
138
+
139
+ this.emit("peer-candidate", { peerId, channelId })
140
+ }
141
+
142
+ receiveMessage(message: Uint8Array) {
143
+ const decoded: InboundMessagePayload = CBOR.decode(new Uint8Array(message))
144
+
145
+ const {
146
+ type,
147
+ senderId,
148
+ targetId,
149
+ channelId,
150
+ message: messageData,
151
+ broadcast,
152
+ } = decoded
153
+
154
+ const socket = this.socket
155
+ if (!socket) {
156
+ throw new Error("Missing socket at receiveMessage")
157
+ }
158
+
159
+ if (message.byteLength === 0) {
160
+ throw new Error("received a zero-length message")
161
+ }
162
+
163
+ switch (type) {
164
+ case "peer":
165
+ log(`peer: ${senderId}, ${channelId}`)
166
+ this.announceConnection(channelId, senderId)
167
+ break
168
+ default:
169
+ this.emit("message", {
170
+ channelId,
171
+ senderId,
172
+ targetId,
173
+ message: new Uint8Array(messageData),
174
+ broadcast,
175
+ })
176
+ }
177
+ }
178
+ }
@@ -0,0 +1,143 @@
1
+ import * as CBOR from "cbor-x"
2
+ import { WebSocket, type WebSocketServer } from "isomorphic-ws"
3
+
4
+ import debug from "debug"
5
+ const log = debug("WebsocketServer")
6
+
7
+ import {
8
+ ChannelId,
9
+ InboundMessagePayload,
10
+ NetworkAdapter,
11
+ PeerId,
12
+ } from "@automerge/automerge-repo"
13
+
14
+ export class NodeWSServerAdapter extends NetworkAdapter {
15
+ server: WebSocketServer
16
+ sockets: { [peerId: PeerId]: WebSocket } = {}
17
+
18
+ constructor(server: WebSocketServer) {
19
+ super()
20
+ this.server = server
21
+ }
22
+
23
+ connect(peerId: PeerId) {
24
+ this.peerId = peerId
25
+ this.server.on("connection", socket => {
26
+ // When a socket closes, or disconnects, remove it from our list
27
+ socket.on("close", () => {
28
+ for (const [otherPeerId, otherSocket] of Object.entries(this.sockets)) {
29
+ if (socket === otherSocket) {
30
+ this.emit("peer-disconnected", { peerId: otherPeerId as PeerId })
31
+ delete this.sockets[otherPeerId as PeerId]
32
+ }
33
+ }
34
+ })
35
+
36
+ socket.on("message", message =>
37
+ this.receiveMessage(message as Uint8Array, socket)
38
+ )
39
+ })
40
+ }
41
+
42
+ join(docId: ChannelId) {
43
+ // throw new Error("The server doesn't join channels.")
44
+ }
45
+
46
+ leave(docId: ChannelId) {
47
+ // throw new Error("The server doesn't join channels.")
48
+ }
49
+
50
+ sendMessage(
51
+ targetId: PeerId,
52
+ channelId: ChannelId,
53
+ message: Uint8Array,
54
+ broadcast: boolean
55
+ ) {
56
+ if (message.byteLength === 0) {
57
+ throw new Error("tried to send a zero-length message")
58
+ }
59
+ const senderId = this.peerId
60
+ if (!senderId) {
61
+ throw new Error("No peerId set for the websocket server network adapter.")
62
+ }
63
+ if (this.sockets[targetId] === undefined) {
64
+ log(`Tried to send message to disconnected peer: ${targetId}`)
65
+ return
66
+ }
67
+
68
+ const decoded: InboundMessagePayload = {
69
+ senderId,
70
+ targetId,
71
+ channelId,
72
+ type: "sync",
73
+ message,
74
+ broadcast,
75
+ }
76
+ const encoded = CBOR.encode(decoded)
77
+
78
+ // This incantation deals with websocket sending the whole
79
+ // underlying buffer even if we just have a uint8array view on it
80
+ const arrayBuf = encoded.buffer.slice(
81
+ encoded.byteOffset,
82
+ encoded.byteOffset + encoded.byteLength
83
+ )
84
+
85
+ log(
86
+ `[${senderId}->${targetId}@${channelId}] "sync" | ${arrayBuf.byteLength} bytes`
87
+ )
88
+
89
+ this.sockets[targetId].send(arrayBuf)
90
+ }
91
+
92
+ receiveMessage(message: Uint8Array, socket: WebSocket) {
93
+ const cbor: InboundMessagePayload = CBOR.decode(message)
94
+
95
+ const {
96
+ type,
97
+ channelId,
98
+ senderId,
99
+ targetId,
100
+ message: data,
101
+ broadcast,
102
+ } = cbor
103
+
104
+ const myPeerId = this.peerId
105
+ if (!myPeerId) {
106
+ throw new Error("Missing my peer ID.")
107
+ }
108
+ log(
109
+ `[${senderId}->${myPeerId}@${channelId}] ${type} | ${message.byteLength} bytes`
110
+ )
111
+ switch (type) {
112
+ case "join":
113
+ // Let the rest of the system know that we have a new connection.
114
+ this.emit("peer-candidate", { peerId: senderId, channelId })
115
+ this.sockets[senderId] = socket
116
+
117
+ // In this client-server connection, there's only ever one peer: us!
118
+ // (and we pretend to be joined to every channel)
119
+ socket.send(
120
+ CBOR.encode({ type: "peer", senderId: this.peerId, channelId })
121
+ )
122
+ break
123
+ case "leave":
124
+ // It doesn't seem like this gets called;
125
+ // we handle leaving in the socket close logic
126
+ // TODO: confirm this
127
+ // ?
128
+ break
129
+ case "message":
130
+ this.emit("message", {
131
+ senderId,
132
+ targetId,
133
+ channelId,
134
+ message: new Uint8Array(data),
135
+ broadcast,
136
+ })
137
+ break
138
+ default:
139
+ // log("unrecognized message type")
140
+ break
141
+ }
142
+ }
143
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { BrowserWebSocketClientAdapter } from "./BrowserWebSocketClientAdapter.js"
2
+ export { NodeWSServerAdapter } from "./NodeWSServerAdapter.js"
@@ -0,0 +1,25 @@
1
+ import { runAdapterTests } from "@automerge/automerge-repo"
2
+ import { BrowserWebSocketClientAdapter } from "../src/BrowserWebSocketClientAdapter"
3
+ import { NodeWSServerAdapter } from "../src/NodeWSServerAdapter"
4
+ import { startServer } from "./utilities/WebSockets"
5
+
6
+ describe("Websocket adapters", async () => {
7
+ let port = 8080
8
+
9
+ runAdapterTests(async () => {
10
+ port += 1 // Increment port to avoid conflicts
11
+ const { socket, server } = await startServer(port)
12
+ const serverAdapter = new NodeWSServerAdapter(socket)
13
+
14
+ const serverUrl = `ws://localhost:${port}`
15
+
16
+ const aliceAdapter = new BrowserWebSocketClientAdapter(serverUrl)
17
+ const bobAdapter = new BrowserWebSocketClientAdapter(serverUrl)
18
+
19
+ const teardown = () => {
20
+ server.close()
21
+ }
22
+
23
+ return { adapters: [serverAdapter, aliceAdapter, bobAdapter], teardown }
24
+ })
25
+ })
@@ -0,0 +1,8 @@
1
+ import http from "http"
2
+ import WebSocket from "ws"
3
+
4
+ function createWebSocketServer(server: http.Server) {
5
+ return new WebSocket.Server({ server })
6
+ }
7
+
8
+ export { createWebSocketServer }
@@ -0,0 +1,34 @@
1
+ import http from "http"
2
+ import { createWebSocketServer } from "./CreateWebSocketServer"
3
+ import WebSocket from "ws"
4
+
5
+ function startServer(port: number) {
6
+ const server = http.createServer()
7
+ const socket = createWebSocketServer(server)
8
+ return new Promise<{
9
+ socket: WebSocket.Server
10
+ server: http.Server
11
+ }>(resolve => {
12
+ server.listen(port, () => resolve({ socket, server }))
13
+ })
14
+ }
15
+
16
+ type WebSocketState =
17
+ | typeof WebSocket.CONNECTING
18
+ | typeof WebSocket.OPEN
19
+ | typeof WebSocket.CLOSING
20
+ | typeof WebSocket.CLOSED
21
+
22
+ function waitForSocketState(socket: WebSocket, state: WebSocketState) {
23
+ return new Promise<void>(function (resolve) {
24
+ setTimeout(function () {
25
+ if (socket.readyState === state) {
26
+ resolve()
27
+ } else {
28
+ waitForSocketState(socket, state).then(resolve)
29
+ }
30
+ }, 5)
31
+ })
32
+ }
33
+
34
+ export { startServer, waitForSocketState }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "jsx": "react",
5
+ "module": "ESNext",
6
+ "moduleResolution": "node",
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "outDir": "./dist",
10
+ "esModuleInterop": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "strict": true,
13
+ "skipLibCheck": true
14
+ },
15
+ "include": ["src/**/*.ts"]
16
+ }