@automerge/automerge-repo-network-websocket 1.0.9 → 1.0.10

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.
@@ -8,6 +8,7 @@ declare abstract class WebSocketNetworkAdapter extends NetworkAdapter {
8
8
  export declare class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
9
9
  #private;
10
10
  timerId?: ReturnType<typeof setTimeout>;
11
+ remotePeerId?: PeerId;
11
12
  url: string;
12
13
  constructor(url: string);
13
14
  connect(peerId: PeerId): void;
@@ -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;AAMtB,uBAAe,uBAAwB,SAAQ,cAAc;IAC3D,MAAM,CAAC,EAAE,SAAS,CAAA;CACnB;AAED,qBAAa,6BAA8B,SAAQ,uBAAuB;;IAGxE,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAA;IAGvC,GAAG,EAAE,MAAM,CAAA;gBAEC,GAAG,EAAE,MAAM;IAKvB,OAAO,CAAC,MAAM,EAAE,MAAM;IAkCtB,MAAM,aAKL;IAGD,OAAO,aAON;IAED,SAAS,UAAW,sBAAsB,UAEzC;IAED,IAAI;IAWJ,UAAU;IAOV,IAAI,CAAC,OAAO,EAAE,iBAAiB;IAwB/B,kBAAkB,CAAC,MAAM,EAAE,MAAM;IAajC,cAAc,CAAC,OAAO,EAAE,UAAU;CA6BnC"}
1
+ {"version":3,"file":"BrowserWebSocketClientAdapter.d.ts","sourceRoot":"","sources":["../src/BrowserWebSocketClientAdapter.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,EAAQ,MAAM,2BAA2B,CAAA;AACxE,OAAO,SAAS,MAAM,eAAe,CAAA;AAIrC,OAAO,EACL,iBAAiB,EAGlB,MAAM,eAAe,CAAA;AAMtB,uBAAe,uBAAwB,SAAQ,cAAc;IAC3D,MAAM,CAAC,EAAE,SAAS,CAAA;CACnB;AAED,qBAAa,6BAA8B,SAAQ,uBAAuB;;IAGxE,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAA;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;CA6BnC"}
@@ -11,6 +11,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
11
11
  // Type trickery required for platform independence,
12
12
  // see https://stackoverflow.com/questions/45802988/typescript-use-correct-version-of-settimeout-node-vs-window
13
13
  timerId;
14
+ remotePeerId; // this adapter only connects to one remote client at a time
14
15
  #startupComplete = false;
15
16
  url;
16
17
  constructor(url) {
@@ -54,6 +55,9 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
54
55
  // When a socket closes, or disconnects, remove it from the array.
55
56
  onClose = () => {
56
57
  log(`${this.url}: close`);
58
+ if (this.remotePeerId) {
59
+ this.emit("peer-disconnected", { peerId: this.remotePeerId });
60
+ }
57
61
  if (!this.timerId) {
58
62
  if (this.peerId) {
59
63
  this.connect(this.peerId);
@@ -106,6 +110,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
106
110
  this.#startupComplete = true;
107
111
  this.emit("ready", { network: this });
108
112
  }
113
+ this.remotePeerId = peerId;
109
114
  this.emit("peer-candidate", { peerId });
110
115
  }
111
116
  receiveMessage(message) {
@@ -50,7 +50,7 @@ export class NodeWSServerAdapter extends NetworkAdapter {
50
50
  socket.isAlive = false;
51
51
  socket.ping();
52
52
  });
53
- }, 30000);
53
+ }, 5000);
54
54
  }
55
55
  disconnect() {
56
56
  // throw new Error("The server doesn't join channels.")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automerge/automerge-repo-network-websocket",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "isomorphic node/browser Websocket network adapter for Automerge Repo",
5
5
  "peerDependencies": {
6
6
  "@automerge/automerge": "^2.1.0"
@@ -16,7 +16,7 @@
16
16
  "test": "vitest"
17
17
  },
18
18
  "dependencies": {
19
- "@automerge/automerge-repo": "^1.0.9",
19
+ "@automerge/automerge-repo": "^1.0.10",
20
20
  "cbor-x": "^1.3.0",
21
21
  "eventemitter3": "^5.0.1",
22
22
  "isomorphic-ws": "^5.0.0",
@@ -33,5 +33,5 @@
33
33
  "publishConfig": {
34
34
  "access": "public"
35
35
  },
36
- "gitHead": "c277a6e8546cdd14236070d033ca62db68dfdeb3"
36
+ "gitHead": "a79cfa659547b7f4a12987bbbee0f4f2f7f219f4"
37
37
  }
@@ -21,6 +21,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
21
21
  // Type trickery required for platform independence,
22
22
  // see https://stackoverflow.com/questions/45802988/typescript-use-correct-version-of-settimeout-node-vs-window
23
23
  timerId?: ReturnType<typeof setTimeout>
24
+ remotePeerId?: PeerId // this adapter only connects to one remote client at a time
24
25
  #startupComplete: boolean = false
25
26
 
26
27
  url: string
@@ -74,6 +75,11 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
74
75
  // When a socket closes, or disconnects, remove it from the array.
75
76
  onClose = () => {
76
77
  log(`${this.url}: close`)
78
+
79
+ if (this.remotePeerId) {
80
+ this.emit("peer-disconnected", { peerId: this.remotePeerId })
81
+ }
82
+
77
83
  if (!this.timerId) {
78
84
  if (this.peerId) {
79
85
  this.connect(this.peerId)
@@ -137,6 +143,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
137
143
  this.#startupComplete = true
138
144
  this.emit("ready", { network: this })
139
145
  }
146
+ this.remotePeerId = peerId
140
147
  this.emit("peer-candidate", { peerId })
141
148
  }
142
149
 
@@ -75,7 +75,7 @@ export class NodeWSServerAdapter extends NetworkAdapter {
75
75
  socket.isAlive = false
76
76
  socket.ping()
77
77
  })
78
- }, 30000)
78
+ }, 5000)
79
79
  }
80
80
 
81
81
  disconnect() {
@@ -1,39 +1,39 @@
1
- import { runAdapterTests } from "../../automerge-repo/src/helpers/tests/network-adapter-tests.js"
2
- import { BrowserWebSocketClientAdapter } from "../src/BrowserWebSocketClientAdapter.js"
3
- import { NodeWSServerAdapter } from "../src/NodeWSServerAdapter.js"
4
- import { startServer } from "./utilities/WebSockets.js"
5
- import * as CBOR from "cbor-x"
6
- import WebSocket, { AddressInfo } from "ws"
7
- import assert from "assert"
8
1
  import { PeerId, Repo } from "@automerge/automerge-repo"
2
+ import assert from "assert"
3
+ import * as CBOR from "cbor-x"
9
4
  import { once } from "events"
5
+ import http from "http"
10
6
  import { describe, it } from "vitest"
7
+ import WebSocket, { AddressInfo } from "ws"
8
+ import { runAdapterTests } from "../../automerge-repo/src/helpers/tests/network-adapter-tests.js"
9
+ import { BrowserWebSocketClientAdapter } from "../src/BrowserWebSocketClientAdapter.js"
10
+ import { NodeWSServerAdapter } from "../src/NodeWSServerAdapter.js"
11
11
 
12
- describe("Websocket adapters", async () => {
13
- let port = 8080
12
+ describe("Websocket adapters", () => {
13
+ const setup = async (clientCount = 1) => {
14
+ const server = http.createServer()
15
+ const socket = new WebSocket.Server({ server })
14
16
 
15
- runAdapterTests(async () => {
16
- let socket: WebSocket.Server | undefined = undefined
17
- let server: any
18
-
19
- while (socket === undefined) {
20
- try {
21
- ;({ socket, server } = await startServer(port))
22
- } catch (e: any) {
23
- if (e.code === "EADDRINUSE") {
24
- port++
25
- } else {
26
- throw e
27
- }
28
- }
29
- }
17
+ await new Promise<void>(resolve => server.listen(0, resolve))
18
+ const { port } = server.address() as AddressInfo
19
+ const serverUrl = `ws://localhost:${port}`
30
20
 
31
- const serverAdapter = new NodeWSServerAdapter(socket)
21
+ const clients = [] as BrowserWebSocketClientAdapter[]
22
+ for (let i = 0; i < clientCount; i++) {
23
+ clients.push(new BrowserWebSocketClientAdapter(serverUrl))
24
+ }
32
25
 
33
- const serverUrl = `ws://localhost:${port}`
26
+ return { socket, server, port, serverUrl, clients }
27
+ }
34
28
 
35
- const aliceAdapter = new BrowserWebSocketClientAdapter(serverUrl)
36
- const bobAdapter = new BrowserWebSocketClientAdapter(serverUrl)
29
+ // run adapter acceptance tests
30
+ runAdapterTests(async () => {
31
+ const {
32
+ clients: [aliceAdapter, bobAdapter],
33
+ socket,
34
+ server,
35
+ } = await setup(2)
36
+ const serverAdapter = new NodeWSServerAdapter(socket)
37
37
 
38
38
  const teardown = () => {
39
39
  server.close()
@@ -41,140 +41,159 @@ describe("Websocket adapters", async () => {
41
41
 
42
42
  return { adapters: [aliceAdapter, serverAdapter, bobAdapter], teardown }
43
43
  })
44
- })
45
44
 
46
- describe("The BrowserWebSocketClientAdapter", () => {
47
- it("should advertise the protocol versions it supports in its join message", async () => {
48
- const { socket, server } = await startServer(0)
49
- let port = (server.address()!! as AddressInfo).port
50
- const serverUrl = `ws://localhost:${port}`
51
- const helloPromise = firstMessage(socket)
45
+ describe("BrowserWebSocketClientAdapter", () => {
46
+ const firstMessage = async (socket: WebSocket.Server<any>) =>
47
+ new Promise((resolve, reject) => {
48
+ socket.once("connection", ws => {
49
+ ws.once("message", (message: any) => resolve(message))
50
+ ws.once("error", (error: any) => reject(error))
51
+ })
52
+ socket.once("error", error => reject(error))
53
+ })
52
54
 
53
- const client = new BrowserWebSocketClientAdapter(serverUrl)
54
- const repo = new Repo({ network: [client], peerId: "browser" as PeerId })
55
+ it("should advertise the protocol versions it supports in its join message", async () => {
56
+ const {
57
+ socket,
58
+ clients: [browser],
59
+ } = await setup()
55
60
 
56
- const hello = await helloPromise
61
+ const helloPromise = firstMessage(socket)
57
62
 
58
- const message = CBOR.decode(hello as Uint8Array)
59
- assert.deepEqual(message, {
60
- type: "join",
61
- senderId: "browser",
62
- supportedProtocolVersions: ["1"],
63
- })
64
- })
65
- })
63
+ const _repo = new Repo({
64
+ network: [browser],
65
+ peerId: "browser" as PeerId,
66
+ })
66
67
 
67
- describe("The NodeWSServerAdapter", () => {
68
- it("should send the negotiated protocol version in its hello message", async () => {
69
- const response = await serverHelloGivenClientHello({
70
- type: "join",
71
- senderId: "browser",
72
- supportedProtocolVersions: ["1"],
73
- })
74
- assert.deepEqual<any>(response, {
75
- type: "peer",
76
- senderId: "server",
77
- targetId: "browser",
78
- selectedProtocolVersion: "1",
68
+ const encodedMessage = await helloPromise
69
+ const message = CBOR.decode(encodedMessage as Uint8Array)
70
+ assert.deepEqual(message, {
71
+ type: "join",
72
+ senderId: "browser",
73
+ supportedProtocolVersions: ["1"],
74
+ })
79
75
  })
80
- })
81
76
 
82
- it("should return an error message if the protocol version is not supported", async () => {
83
- const response = await serverHelloGivenClientHello({
84
- type: "join",
85
- senderId: "browser",
86
- supportedProtocolVersions: ["fake"],
77
+ it.skip("should announce disconnections", async () => {
78
+ const {
79
+ server,
80
+ socket,
81
+ clients: [browserAdapter],
82
+ } = await setup()
83
+
84
+ const _browserRepo = new Repo({
85
+ network: [browserAdapter],
86
+ peerId: "browser" as PeerId,
87
+ })
88
+
89
+ const serverAdapter = new NodeWSServerAdapter(socket)
90
+ const _serverRepo = new Repo({
91
+ network: [serverAdapter],
92
+ peerId: "server" as PeerId,
93
+ })
94
+
95
+ const disconnectPromise = new Promise<void>(resolve => {
96
+ browserAdapter.on("peer-disconnected", () => resolve())
97
+ })
98
+
99
+ server.close()
100
+ await disconnectPromise
87
101
  })
88
- assert.deepEqual<any>(response, {
89
- type: "error",
90
- senderId: "server",
91
- targetId: "browser",
92
- message: "unsupported protocol version",
102
+
103
+ it("should correctly clear event handlers on reconnect", async () => {
104
+ // This reproduces an issue where the BrowserWebSocketClientAdapter.connect
105
+ // call registered event handlers on the websocket but didn't clear them
106
+ // up again on a second call to connect. This combined with the reconnect
107
+ // timer to produce the following sequence of events:
108
+ //
109
+ // - first call to connect creates a socket and registers an "open"
110
+ // handler. The "open" handler will attempt to send a join message
111
+ // - The connection is slow, so the reconnect timer fires before "open",
112
+ // the reconnect timer calls connect again. this.socket is now a new socket
113
+ // - The other end replies to the first socket, "open"
114
+ // - The original "open" handler attempts to send a message, but on the new
115
+ // socket (because it uses `this.socket`), which isn't open yet, so we get an
116
+ // error that the socket is not ready
117
+ const {
118
+ clients: [browser],
119
+ } = await setup()
120
+
121
+ const peerId = "testclient" as PeerId
122
+ browser.connect(peerId)
123
+
124
+ // simulate the reconnect timer firing before the other end has responded
125
+ // (which works here because we haven't yielded to the event loop yet so
126
+ // the server, which is on the same event loop as us, can't respond)
127
+ browser.connect(peerId)
128
+
129
+ // Now yield, so the server responds on the first socket, if the listeners
130
+ // are cleaned up correctly we shouldn't throw
131
+ await pause(1)
93
132
  })
94
133
  })
95
134
 
96
- it("should respond with protocol v1 if no protocol version is specified", async () => {
97
- const response = await serverHelloGivenClientHello({
98
- type: "join",
99
- senderId: "browser",
100
- })
101
- assert.deepEqual<any>(response, {
102
- type: "peer",
103
- senderId: "server",
104
- targetId: "browser",
105
- selectedProtocolVersion: "1",
135
+ describe("NodeWSServerAdapter", () => {
136
+ const serverResponse = async (clientHello: Object) => {
137
+ const { socket, serverUrl } = await setup(0)
138
+ const server = new NodeWSServerAdapter(socket)
139
+ const _serverRepo = new Repo({
140
+ network: [server],
141
+ peerId: "server" as PeerId,
142
+ })
143
+
144
+ const clientSocket = new WebSocket(serverUrl)
145
+ await once(clientSocket, "open")
146
+ const serverHelloPromise = once(clientSocket, "message")
147
+
148
+ clientSocket.send(CBOR.encode(clientHello))
149
+
150
+ const serverHello = await serverHelloPromise
151
+ const message = CBOR.decode(serverHello[0] as Uint8Array)
152
+ return message
153
+ }
154
+
155
+ it("should send the negotiated protocol version in its hello message", async () => {
156
+ const response = await serverResponse({
157
+ type: "join",
158
+ senderId: "browser",
159
+ supportedProtocolVersions: ["1"],
160
+ })
161
+ assert.deepEqual(response, {
162
+ type: "peer",
163
+ senderId: "server",
164
+ targetId: "browser",
165
+ selectedProtocolVersion: "1",
166
+ })
106
167
  })
107
- })
108
-
109
- it("should correctly clear event handlers on reconnect", async () => {
110
- // This reproduces an issue where the BrowserWebSocketClientAdapter.connect
111
- // call registered event handlers on the websocket but didn't clear them
112
- // up again on a second call to connect. This combined with the reconnect
113
- // timer to produce the following sequence of events:
114
- //
115
- // * first call to connect creates a socket and registers an "open"
116
- // handler. The "open" handler will attempt to send a join message
117
- // * The connection is slow, so the reconnect timer fires before "open",
118
- // the reconnect timer calls connect again. this.socket is now a new socket
119
- // * The other end replies to the first socket, "open"
120
- // * The original "open" handler attempts to send a message, but on the new
121
- // socket (because it uses this.socket), which isn't open yet, so we get an
122
- // error that the socket is not ready
123
- const { socket, server } = await startServer(0)
124
- let port = (server.address()!! as AddressInfo).port
125
- const serverUrl = `ws://localhost:${port}`
126
- const serverAdapter = new NodeWSServerAdapter(socket)
127
- const browserAdapter = new BrowserWebSocketClientAdapter(serverUrl)
128
-
129
- const peerId = "testclient" as PeerId
130
- browserAdapter.connect(peerId)
131
- // simulate the reconnect timer firing before the other end has responded
132
- // (which works here because we haven't yielded to the event loop yet so
133
- // the server, which is on the same event loop as us, can't respond)
134
- browserAdapter.connect(peerId)
135
- // Now await, so the server responds on the first socket, if the listeners
136
- // are cleaned up correctly we shouldn't throw
137
- await pause(1)
138
- })
139
- })
140
168
 
141
- async function serverHelloGivenClientHello(
142
- clientHello: Object
143
- ): Promise<Object | null> {
144
- const { socket, server } = await startServer(0)
145
- let port = (server.address()!! as AddressInfo).port
146
- const serverUrl = `ws://localhost:${port}`
147
- const adapter = new NodeWSServerAdapter(socket)
148
- const repo = new Repo({ network: [adapter], peerId: "server" as PeerId })
149
-
150
- const clientSocket = new WebSocket(serverUrl)
151
- await once(clientSocket, "open")
152
- const serverHelloPromise = once(clientSocket, "message")
153
-
154
- clientSocket.send(CBOR.encode(clientHello))
155
-
156
- const serverHello = await serverHelloPromise
157
- const message = CBOR.decode(serverHello[0] as Uint8Array)
158
- return message
159
- }
160
-
161
- async function firstMessage(
162
- socket: WebSocket.Server<any>
163
- ): Promise<Object | null> {
164
- return new Promise((resolve, reject) => {
165
- socket.once("connection", ws => {
166
- ws.once("message", (message: any) => {
167
- resolve(message)
169
+ it("should return an error message if the protocol version is not supported", async () => {
170
+ const response = await serverResponse({
171
+ type: "join",
172
+ senderId: "browser",
173
+ supportedProtocolVersions: ["fake"],
168
174
  })
169
- ws.once("error", (error: any) => {
170
- reject(error)
175
+ assert.deepEqual(response, {
176
+ type: "error",
177
+ senderId: "server",
178
+ targetId: "browser",
179
+ message: "unsupported protocol version",
171
180
  })
172
181
  })
173
- socket.once("error", error => {
174
- reject(error)
182
+
183
+ it("should respond with protocol v1 if no protocol version is specified", async () => {
184
+ const response = await serverResponse({
185
+ type: "join",
186
+ senderId: "browser",
187
+ })
188
+ assert.deepEqual(response, {
189
+ type: "peer",
190
+ senderId: "server",
191
+ targetId: "browser",
192
+ selectedProtocolVersion: "1",
193
+ })
175
194
  })
176
195
  })
177
- }
196
+ })
178
197
 
179
198
  export const pause = (t = 0) =>
180
199
  new Promise<void>(resolve => setTimeout(() => resolve(), t))
@@ -1,8 +0,0 @@
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 }
@@ -1,34 +0,0 @@
1
- import http from "http"
2
- import { createWebSocketServer } from "./CreateWebSocketServer.js"
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 }