@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.
- package/dist/BrowserWebSocketClientAdapter.d.ts +8 -7
- package/dist/BrowserWebSocketClientAdapter.d.ts.map +1 -1
- package/dist/BrowserWebSocketClientAdapter.js +95 -92
- package/dist/NodeWSServerAdapter.d.ts +7 -5
- package/dist/NodeWSServerAdapter.d.ts.map +1 -1
- package/dist/NodeWSServerAdapter.js +107 -107
- 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 +9 -11
- 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 +101 -123
- package/src/NodeWSServerAdapter.ts +121 -132
- package/src/assert.ts +28 -0
- package/src/messages.ts +23 -16
- package/src/toArrayBuffer.ts +8 -0
- package/test/Websocket.test.ts +353 -131
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,
|
|
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.0-alpha.
|
|
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": "
|
|
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": "
|
|
34
|
+
"gitHead": "f4ce1376d900ad98f00a638626be9611077460b5"
|
|
34
35
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
NetworkAdapter,
|
|
3
|
-
|
|
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
|
-
|
|
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: boolean = 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(
|
|
40
|
-
peerId
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
//
|
|
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(
|
|
84
|
-
clearInterval(this
|
|
85
|
-
this
|
|
86
|
-
this.
|
|
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(
|
|
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
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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.
|
|
123
|
+
this.send(joinMessage(this.peerId!, this.peerMetadata!))
|
|
114
124
|
} else {
|
|
115
|
-
//
|
|
125
|
+
// We'll try again in the `onOpen` handler
|
|
116
126
|
}
|
|
117
127
|
}
|
|
118
128
|
|
|
119
129
|
disconnect() {
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
128
|
-
throw new Error("
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (
|
|
132
|
-
throw new Error(
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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(
|
|
169
|
-
const
|
|
157
|
+
receiveMessage(messageBytes: Uint8Array) {
|
|
158
|
+
const message: FromServerMessage = cbor.decode(new Uint8Array(messageBytes))
|
|
170
159
|
|
|
171
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
201
|
-
isEphemeral: boolean
|
|
178
|
+
peerMetadata: PeerMetadata
|
|
202
179
|
): JoinMessage {
|
|
203
180
|
return {
|
|
204
181
|
type: "join",
|
|
205
182
|
senderId,
|
|
206
|
-
|
|
207
|
-
isEphemeral,
|
|
183
|
+
peerMetadata,
|
|
208
184
|
supportedProtocolVersions: [ProtocolV1],
|
|
209
185
|
}
|
|
210
186
|
}
|
|
187
|
+
|
|
188
|
+
type TimeoutId = ReturnType<typeof setTimeout> // https://stackoverflow.com/questions/45802988
|