@automerge/automerge-repo-network-websocket 1.1.0-alpha.1 → 1.1.0-alpha.13

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.
@@ -1 +1 @@
1
- {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAA;AAC3E,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,4FAA4F;IAC5F,SAAS,CAAC,EAAE,SAAS,CAAA;IAErB;8DAC0D;IAC1D,WAAW,EAAE,OAAO,CAAA;IAEpB,+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,4FAA4F;IAC5F,SAAS,CAAC,EAAE,SAAS,CAAA;IAErB;8DAC0D;IAC1D,WAAW,EAAE,OAAO,CAAA;IAEpB,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.1",
3
+ "version": "1.1.0-alpha.13",
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.1",
16
+ "@automerge/automerge-repo": "1.1.0-alpha.13",
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": "11805d698f860bd6ffb3ca028d3b57e718690b5a"
34
+ "gitHead": "f4ce1376d900ad98f00a638626be9611077460b5"
34
35
  }
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  NetworkAdapter,
3
- type PeerId,
3
+ PeerId,
4
+ PeerMetadata,
4
5
  cbor,
5
- type StorageId,
6
6
  } from "@automerge/automerge-repo"
7
7
  import WebSocket from "isomorphic-ws"
8
8
 
@@ -12,199 +12,177 @@ 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: boolean = 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(
40
- peerId: PeerId,
41
- storageId: StorageId | undefined,
42
- isEphemeral: boolean
43
- ) {
44
- // If we're reconnecting make sure we remove the old event listeners
45
- // before creating a new connection.
46
- 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.
47
51
  this.socket.removeEventListener("open", this.onOpen)
48
52
  this.socket.removeEventListener("close", this.onClose)
49
53
  this.socket.removeEventListener("message", this.onMessage)
54
+ this.socket.removeEventListener("error", this.onError)
50
55
  }
56
+ // Wire up retries
57
+ if (!this.#retryIntervalId)
58
+ this.#retryIntervalId = setInterval(() => {
59
+ this.connect(peerId, peerMetadata)
60
+ }, this.retryInterval)
51
61
 
52
- if (!this.timerId) {
53
- this.timerId = setInterval(
54
- () => this.connect(peerId, storageId, isEphemeral),
55
- 5000
56
- )
57
- }
58
-
59
- this.peerId = peerId
60
- this.storageId = storageId
61
- this.isEphemeral = isEphemeral
62
62
  this.socket = new WebSocket(this.url)
63
+
63
64
  this.socket.binaryType = "arraybuffer"
64
65
 
65
66
  this.socket.addEventListener("open", this.onOpen)
66
67
  this.socket.addEventListener("close", this.onClose)
67
68
  this.socket.addEventListener("message", this.onMessage)
69
+ this.socket.addEventListener("error", this.onError)
68
70
 
69
- // 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.
70
72
  // We might hear back from the other end at some point but we shouldn't
71
73
  // hold up marking things as unavailable for any longer
72
- setTimeout(() => {
73
- if (!this.#startupComplete) {
74
- this.#startupComplete = true
75
- this.emit("ready", { network: this })
76
- }
77
- }, 1000)
78
-
74
+ setTimeout(() => this.#ready(), 1000)
79
75
  this.join()
80
76
  }
81
77
 
82
78
  onOpen = () => {
83
- log(`@ ${this.url}: open`)
84
- clearInterval(this.timerId)
85
- this.timerId = undefined
86
- this.send(joinMessage(this.peerId!, this.storageId, this.isEphemeral))
79
+ this.#log("open")
80
+ clearInterval(this.#retryIntervalId)
81
+ this.#retryIntervalId = undefined
82
+ this.join()
87
83
  }
88
84
 
89
85
  // When a socket closes, or disconnects, remove it from the array.
90
86
  onClose = () => {
91
- log(`${this.url}: close`)
92
-
93
- if (this.remotePeerId) {
87
+ this.#log("close")
88
+ if (this.remotePeerId)
94
89
  this.emit("peer-disconnected", { peerId: this.remotePeerId })
95
- }
96
90
 
97
- if (!this.timerId) {
98
- if (this.peerId) {
99
- this.connect(this.peerId, this.storageId, this.isEphemeral)
100
- }
101
- }
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)
102
97
  }
103
98
 
104
99
  onMessage = (event: WebSocket.MessageEvent) => {
105
100
  this.receiveMessage(event.data as Uint8Array)
106
101
  }
107
102
 
108
- join() {
109
- if (!this.socket) {
110
- 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
111
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)
112
122
  if (this.socket.readyState === WebSocket.OPEN) {
113
- this.send(joinMessage(this.peerId!, this.storageId, this.isEphemeral))
123
+ this.send(joinMessage(this.peerId!, this.peerMetadata!))
114
124
  } else {
115
- // The onOpen handler automatically sends a join message
125
+ // We'll try again in the `onOpen` handler
116
126
  }
117
127
  }
118
128
 
119
129
  disconnect() {
120
- if (!this.socket) {
121
- throw new Error("WTF, get a socket")
122
- }
123
- this.send({ type: "leave", senderId: this.peerId! })
130
+ assert(this.peerId)
131
+ assert(this.socket)
132
+ this.send({ type: "leave", senderId: this.peerId })
124
133
  }
125
134
 
126
135
  send(message: FromClientMessage) {
127
- if ("data" in message && message.data.byteLength === 0) {
128
- throw new Error("tried to send a zero-length message")
129
- }
130
-
131
- if (!this.peerId) {
132
- throw new Error("Why don't we have a PeerID?")
133
- }
134
-
135
- if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
136
- throw new Error("Websocket Socket not ready!")
137
- }
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})`)
138
142
 
139
143
  const encoded = cbor.encode(message)
140
- // This incantation deals with websocket sending the whole
141
- // underlying buffer even if we just have a uint8array view on it
142
- const arrayBuf = encoded.buffer.slice(
143
- encoded.byteOffset,
144
- encoded.byteOffset + encoded.byteLength
145
- )
146
-
147
- this.socket?.send(arrayBuf)
144
+ this.socket.send(toArrayBuffer(encoded))
148
145
  }
149
146
 
150
- announceConnection(
151
- peerId: PeerId,
152
- storageId: StorageId | undefined,
153
- isEphemeral: boolean
154
- ) {
155
- // return a peer object
156
- const myPeerId = this.peerId
157
- if (!myPeerId) {
158
- throw new Error("we should have a peer ID by now")
159
- }
160
- if (!this.#startupComplete) {
161
- this.#startupComplete = true
162
- this.emit("ready", { network: this })
163
- }
164
- this.remotePeerId = peerId
165
- this.emit("peer-candidate", { peerId, storageId, isEphemeral })
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
+ })
166
155
  }
167
156
 
168
- receiveMessage(message: Uint8Array) {
169
- const decoded: FromServerMessage = cbor.decode(new Uint8Array(message))
157
+ receiveMessage(messageBytes: Uint8Array) {
158
+ const message: FromServerMessage = cbor.decode(new Uint8Array(messageBytes))
170
159
 
171
- const { type, senderId } = decoded
172
-
173
- const socket = this.socket
174
- if (!socket) {
175
- throw new Error("Missing socket at receiveMessage")
176
- }
177
-
178
- if (message.byteLength === 0) {
160
+ assert(this.socket)
161
+ if (messageBytes.byteLength === 0)
179
162
  throw new Error("received a zero-length message")
180
- }
181
163
 
182
- switch (type) {
183
- case "peer": {
184
- const { storageId, isEphemeral } = decoded
185
- log(`peer: ${senderId}`)
186
- this.announceConnection(senderId, storageId, isEphemeral)
187
- break
188
- }
189
- case "error":
190
- log(`error: ${decoded.message}`)
191
- break
192
- default:
193
- 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)
194
172
  }
195
173
  }
196
174
  }
197
175
 
198
176
  function joinMessage(
199
177
  senderId: PeerId,
200
- storageId: StorageId | undefined,
201
- isEphemeral: boolean
178
+ peerMetadata: PeerMetadata
202
179
  ): JoinMessage {
203
180
  return {
204
181
  type: "join",
205
182
  senderId,
206
- storageId,
207
- isEphemeral,
183
+ peerMetadata,
208
184
  supportedProtocolVersions: [ProtocolV1],
209
185
  }
210
186
  }
187
+
188
+ type TimeoutId = ReturnType<typeof setTimeout> // https://stackoverflow.com/questions/45802988