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

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.
@@ -11,6 +11,9 @@ export declare class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapt
11
11
  url: string;
12
12
  constructor(url: string);
13
13
  connect(peerId: PeerId): void;
14
+ onOpen: () => void;
15
+ onClose: () => void;
16
+ onMessage: (event: WebSocket.MessageEvent) => void;
14
17
  join(): void;
15
18
  disconnect(): void;
16
19
  send(message: FromClientMessage): 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;IA0CtB,IAAI;IAoBJ,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;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"}
@@ -18,27 +18,22 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
18
18
  this.url = url;
19
19
  }
20
20
  connect(peerId) {
21
- this.socket?.removeAllListeners();
21
+ // If we're reconnecting make sure we remove the old event listeners
22
+ // before creating a new connection.
23
+ if (this.socket) {
24
+ this.socket.removeEventListener("open", this.onOpen);
25
+ this.socket.removeEventListener("close", this.onClose);
26
+ this.socket.removeEventListener("message", this.onMessage);
27
+ }
22
28
  if (!this.timerId) {
23
29
  this.timerId = setInterval(() => this.connect(peerId), 5000);
24
30
  }
25
31
  this.peerId = peerId;
26
32
  this.socket = new WebSocket(this.url);
27
33
  this.socket.binaryType = "arraybuffer";
28
- this.socket.addEventListener("open", () => {
29
- log(`@ ${this.url}: open`);
30
- clearInterval(this.timerId);
31
- this.timerId = undefined;
32
- });
33
- // When a socket closes, or disconnects, remove it from the array.
34
- this.socket.addEventListener("close", () => {
35
- log(`${this.url}: close`);
36
- if (!this.timerId) {
37
- this.connect(peerId);
38
- }
39
- // log("Disconnected from server")
40
- });
41
- this.socket.addEventListener("message", (event) => this.receiveMessage(event.data));
34
+ this.socket.addEventListener("open", this.onOpen);
35
+ this.socket.addEventListener("close", this.onClose);
36
+ this.socket.addEventListener("message", this.onMessage);
42
37
  // mark this adapter as ready if we haven't received an ack in 1 second.
43
38
  // We might hear back from the other end at some point but we shouldn't
44
39
  // hold up marking things as unavailable for any longer
@@ -50,6 +45,24 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
50
45
  }, 1000);
51
46
  this.join();
52
47
  }
48
+ onOpen = () => {
49
+ log(`@ ${this.url}: open`);
50
+ clearInterval(this.timerId);
51
+ this.timerId = undefined;
52
+ this.send(joinMessage(this.peerId));
53
+ };
54
+ // When a socket closes, or disconnects, remove it from the array.
55
+ onClose = () => {
56
+ log(`${this.url}: close`);
57
+ if (!this.timerId) {
58
+ if (this.peerId) {
59
+ this.connect(this.peerId);
60
+ }
61
+ }
62
+ };
63
+ onMessage = (event) => {
64
+ this.receiveMessage(event.data);
65
+ };
53
66
  join() {
54
67
  if (!this.socket) {
55
68
  throw new Error("WTF, get a socket");
@@ -58,12 +71,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
58
71
  this.send(joinMessage(this.peerId));
59
72
  }
60
73
  else {
61
- this.socket.addEventListener("open", () => {
62
- if (!this.socket) {
63
- throw new Error("WTF, get a socket");
64
- }
65
- this.send(joinMessage(this.peerId));
66
- }, { once: true });
74
+ // The onOpen handler automatically sends a join message
67
75
  }
68
76
  }
69
77
  disconnect() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automerge/automerge-repo-network-websocket",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
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.7",
19
+ "@automerge/automerge-repo": "^1.0.9",
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": "71060981f168e511a99ab85b155a54a13fd04bcc"
36
+ "gitHead": "c277a6e8546cdd14236070d033ca62db68dfdeb3"
37
37
  }
@@ -31,7 +31,14 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
31
31
  }
32
32
 
33
33
  connect(peerId: PeerId) {
34
- this.socket?.removeAllListeners()
34
+ // If we're reconnecting make sure we remove the old event listeners
35
+ // before creating a new connection.
36
+ if (this.socket) {
37
+ this.socket.removeEventListener("open", this.onOpen)
38
+ this.socket.removeEventListener("close", this.onClose)
39
+ this.socket.removeEventListener("message", this.onMessage)
40
+ }
41
+
35
42
  if (!this.timerId) {
36
43
  this.timerId = setInterval(() => this.connect(peerId), 5000)
37
44
  }
@@ -40,24 +47,9 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
40
47
  this.socket = new WebSocket(this.url)
41
48
  this.socket.binaryType = "arraybuffer"
42
49
 
43
- this.socket.addEventListener("open", () => {
44
- log(`@ ${this.url}: open`)
45
- clearInterval(this.timerId)
46
- this.timerId = undefined
47
- })
48
-
49
- // When a socket closes, or disconnects, remove it from the array.
50
- this.socket.addEventListener("close", () => {
51
- log(`${this.url}: close`)
52
- if (!this.timerId) {
53
- this.connect(peerId)
54
- }
55
- // log("Disconnected from server")
56
- })
57
-
58
- this.socket.addEventListener("message", (event: WebSocket.MessageEvent) =>
59
- this.receiveMessage(event.data as Uint8Array)
60
- )
50
+ this.socket.addEventListener("open", this.onOpen)
51
+ this.socket.addEventListener("close", this.onClose)
52
+ this.socket.addEventListener("message", this.onMessage)
61
53
 
62
54
  // mark this adapter as ready if we haven't received an ack in 1 second.
63
55
  // We might hear back from the other end at some point but we shouldn't
@@ -72,6 +64,27 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
72
64
  this.join()
73
65
  }
74
66
 
67
+ onOpen = () => {
68
+ log(`@ ${this.url}: open`)
69
+ clearInterval(this.timerId)
70
+ this.timerId = undefined
71
+ this.send(joinMessage(this.peerId!))
72
+ }
73
+
74
+ // When a socket closes, or disconnects, remove it from the array.
75
+ onClose = () => {
76
+ log(`${this.url}: close`)
77
+ if (!this.timerId) {
78
+ if (this.peerId) {
79
+ this.connect(this.peerId)
80
+ }
81
+ }
82
+ }
83
+
84
+ onMessage = (event: WebSocket.MessageEvent) => {
85
+ this.receiveMessage(event.data as Uint8Array)
86
+ }
87
+
75
88
  join() {
76
89
  if (!this.socket) {
77
90
  throw new Error("WTF, get a socket")
@@ -79,16 +92,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
79
92
  if (this.socket.readyState === WebSocket.OPEN) {
80
93
  this.send(joinMessage(this.peerId!))
81
94
  } else {
82
- this.socket.addEventListener(
83
- "open",
84
- () => {
85
- if (!this.socket) {
86
- throw new Error("WTF, get a socket")
87
- }
88
- this.send(joinMessage(this.peerId!))
89
- },
90
- { once: true }
91
- )
95
+ // The onOpen handler automatically sends a join message
92
96
  }
93
97
  }
94
98
 
@@ -105,6 +105,37 @@ describe("The NodeWSServerAdapter", () => {
105
105
  selectedProtocolVersion: "1",
106
106
  })
107
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
+ })
108
139
  })
109
140
 
110
141
  async function serverHelloGivenClientHello(
@@ -144,3 +175,6 @@ async function firstMessage(
144
175
  })
145
176
  })
146
177
  }
178
+
179
+ export const pause = (t = 0) =>
180
+ new Promise<void>(resolve => setTimeout(() => resolve(), t))