@automerge/automerge-repo-network-websocket 1.1.0-alpha.7 → 1.1.0
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 +91 -86
- package/dist/NodeWSServerAdapter.d.ts +5 -3
- package/dist/NodeWSServerAdapter.d.ts.map +1 -1
- package/dist/NodeWSServerAdapter.js +105 -102
- 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 +4 -0
- 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 +98 -106
- package/src/NodeWSServerAdapter.ts +118 -122
- package/src/assert.ts +28 -0
- package/src/messages.ts +18 -3
- package/src/toArrayBuffer.ts +8 -0
- package/test/Websocket.test.ts +350 -125
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automerge/automerge-repo-network-websocket",
|
|
3
|
-
"version": "1.1.0
|
|
3
|
+
"version": "1.1.0",
|
|
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",
|
|
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": "e9e7d3f27ec2ac8a2e9d122ece80598918940067"
|
|
34
35
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
NetworkAdapter,
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
PeerId,
|
|
4
|
+
PeerMetadata,
|
|
5
5
|
cbor,
|
|
6
6
|
} from "@automerge/automerge-repo"
|
|
7
7
|
import WebSocket from "isomorphic-ws"
|
|
@@ -12,173 +12,163 @@ 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(peerId: PeerId, peerMetadata
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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.
|
|
43
51
|
this.socket.removeEventListener("open", this.onOpen)
|
|
44
52
|
this.socket.removeEventListener("close", this.onClose)
|
|
45
53
|
this.socket.removeEventListener("message", this.onMessage)
|
|
54
|
+
this.socket.removeEventListener("error", this.onError)
|
|
46
55
|
}
|
|
56
|
+
// Wire up retries
|
|
57
|
+
if (!this.#retryIntervalId)
|
|
58
|
+
this.#retryIntervalId = setInterval(() => {
|
|
59
|
+
this.connect(peerId, peerMetadata)
|
|
60
|
+
}, this.retryInterval)
|
|
47
61
|
|
|
48
|
-
if (!this.timerId) {
|
|
49
|
-
this.timerId = setInterval(() => this.connect(peerId, peerMetadata), 5000)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
this.peerId = peerId
|
|
53
|
-
this.peerMetadata = peerMetadata
|
|
54
62
|
this.socket = new WebSocket(this.url)
|
|
63
|
+
|
|
55
64
|
this.socket.binaryType = "arraybuffer"
|
|
56
65
|
|
|
57
66
|
this.socket.addEventListener("open", this.onOpen)
|
|
58
67
|
this.socket.addEventListener("close", this.onClose)
|
|
59
68
|
this.socket.addEventListener("message", this.onMessage)
|
|
69
|
+
this.socket.addEventListener("error", this.onError)
|
|
60
70
|
|
|
61
|
-
//
|
|
71
|
+
// Mark this adapter as ready if we haven't received an ack in 1 second.
|
|
62
72
|
// We might hear back from the other end at some point but we shouldn't
|
|
63
73
|
// hold up marking things as unavailable for any longer
|
|
64
|
-
setTimeout(() =>
|
|
65
|
-
if (!this.#startupComplete) {
|
|
66
|
-
this.#startupComplete = true
|
|
67
|
-
this.emit("ready", { network: this })
|
|
68
|
-
}
|
|
69
|
-
}, 1000)
|
|
70
|
-
|
|
74
|
+
setTimeout(() => this.#ready(), 1000)
|
|
71
75
|
this.join()
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
onOpen = () => {
|
|
75
|
-
log(
|
|
76
|
-
clearInterval(this
|
|
77
|
-
this
|
|
78
|
-
this.
|
|
79
|
+
this.#log("open")
|
|
80
|
+
clearInterval(this.#retryIntervalId)
|
|
81
|
+
this.#retryIntervalId = undefined
|
|
82
|
+
this.join()
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
// When a socket closes, or disconnects, remove it from the array.
|
|
82
86
|
onClose = () => {
|
|
83
|
-
log(
|
|
84
|
-
|
|
85
|
-
if (this.remotePeerId) {
|
|
87
|
+
this.#log("close")
|
|
88
|
+
if (this.remotePeerId)
|
|
86
89
|
this.emit("peer-disconnected", { peerId: this.remotePeerId })
|
|
87
|
-
}
|
|
88
90
|
|
|
89
|
-
if (!this
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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)
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
onMessage = (event: WebSocket.MessageEvent) => {
|
|
97
100
|
this.receiveMessage(event.data as Uint8Array)
|
|
98
101
|
}
|
|
99
102
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
103
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)
|
|
104
122
|
if (this.socket.readyState === WebSocket.OPEN) {
|
|
105
123
|
this.send(joinMessage(this.peerId!, this.peerMetadata!))
|
|
106
124
|
} else {
|
|
107
|
-
//
|
|
125
|
+
// We'll try again in the `onOpen` handler
|
|
108
126
|
}
|
|
109
127
|
}
|
|
110
128
|
|
|
111
129
|
disconnect() {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
this.send({ type: "leave", senderId: this.peerId! })
|
|
130
|
+
assert(this.peerId)
|
|
131
|
+
assert(this.socket)
|
|
132
|
+
this.send({ type: "leave", senderId: this.peerId })
|
|
116
133
|
}
|
|
117
134
|
|
|
118
135
|
send(message: FromClientMessage) {
|
|
119
|
-
if ("data" in message && message.data
|
|
120
|
-
throw new Error("
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
throw new Error(
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
128
|
-
throw new Error("Websocket Socket not ready!")
|
|
129
|
-
}
|
|
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})`)
|
|
130
142
|
|
|
131
143
|
const encoded = cbor.encode(message)
|
|
132
|
-
|
|
133
|
-
// underlying buffer even if we just have a uint8array view on it
|
|
134
|
-
const arrayBuf = encoded.buffer.slice(
|
|
135
|
-
encoded.byteOffset,
|
|
136
|
-
encoded.byteOffset + encoded.byteLength
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
this.socket?.send(arrayBuf)
|
|
144
|
+
this.socket.send(toArrayBuffer(encoded))
|
|
140
145
|
}
|
|
141
146
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
this.emit("ready", { network: this })
|
|
151
|
-
}
|
|
152
|
-
this.remotePeerId = peerId
|
|
153
|
-
this.emit("peer-candidate", { peerId, peerMetadata })
|
|
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
|
+
})
|
|
154
155
|
}
|
|
155
156
|
|
|
156
|
-
receiveMessage(
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
const { type, senderId } = decoded
|
|
160
|
-
|
|
161
|
-
const socket = this.socket
|
|
162
|
-
if (!socket) {
|
|
163
|
-
throw new Error("Missing socket at receiveMessage")
|
|
164
|
-
}
|
|
157
|
+
receiveMessage(messageBytes: Uint8Array) {
|
|
158
|
+
const message: FromServerMessage = cbor.decode(new Uint8Array(messageBytes))
|
|
165
159
|
|
|
166
|
-
|
|
160
|
+
assert(this.socket)
|
|
161
|
+
if (messageBytes.byteLength === 0)
|
|
167
162
|
throw new Error("received a zero-length message")
|
|
168
|
-
}
|
|
169
163
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
log(`error: ${decoded.message}`)
|
|
179
|
-
break
|
|
180
|
-
default:
|
|
181
|
-
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)
|
|
182
172
|
}
|
|
183
173
|
}
|
|
184
174
|
}
|
|
@@ -194,3 +184,5 @@ function joinMessage(
|
|
|
194
184
|
supportedProtocolVersions: [ProtocolV1],
|
|
195
185
|
}
|
|
196
186
|
}
|
|
187
|
+
|
|
188
|
+
type TimeoutId = ReturnType<typeof setTimeout> // https://stackoverflow.com/questions/45802988
|
|
@@ -10,41 +10,41 @@ import {
|
|
|
10
10
|
type PeerMetadata,
|
|
11
11
|
type PeerId,
|
|
12
12
|
} from "@automerge/automerge-repo"
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
FromClientMessage,
|
|
15
|
+
FromServerMessage,
|
|
16
|
+
isJoinMessage,
|
|
17
|
+
isLeaveMessage,
|
|
18
|
+
} from "./messages.js"
|
|
14
19
|
import { ProtocolV1, ProtocolVersion } from "./protocolVersion.js"
|
|
20
|
+
import assert from "assert"
|
|
21
|
+
import { toArrayBuffer } from "./toArrayBuffer.js"
|
|
15
22
|
|
|
16
23
|
const { encode, decode } = cborHelpers
|
|
17
24
|
|
|
18
|
-
interface WebSocketWithIsAlive extends WebSocket {
|
|
19
|
-
isAlive: boolean
|
|
20
|
-
}
|
|
21
|
-
|
|
22
25
|
export class NodeWSServerAdapter extends NetworkAdapter {
|
|
23
|
-
server: WebSocketServer
|
|
24
26
|
sockets: { [peerId: PeerId]: WebSocket } = {}
|
|
25
27
|
|
|
26
|
-
constructor(
|
|
28
|
+
constructor(
|
|
29
|
+
private server: WebSocketServer,
|
|
30
|
+
private keepAliveInterval = 5000
|
|
31
|
+
) {
|
|
27
32
|
super()
|
|
28
|
-
this.server = server
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
connect(peerId: PeerId, peerMetadata: PeerMetadata) {
|
|
32
36
|
this.peerId = peerId
|
|
33
37
|
this.peerMetadata = peerMetadata
|
|
34
38
|
|
|
35
|
-
this.server.on("close",
|
|
36
|
-
clearInterval(
|
|
39
|
+
this.server.on("close", () => {
|
|
40
|
+
clearInterval(keepAliveId)
|
|
41
|
+
this.disconnect()
|
|
37
42
|
})
|
|
38
43
|
|
|
39
44
|
this.server.on("connection", (socket: WebSocketWithIsAlive) => {
|
|
40
45
|
// When a socket closes, or disconnects, remove it from our list
|
|
41
46
|
socket.on("close", () => {
|
|
42
|
-
|
|
43
|
-
if (socket === otherSocket) {
|
|
44
|
-
this.emit("peer-disconnected", { peerId: otherPeerId as PeerId })
|
|
45
|
-
delete this.sockets[otherPeerId as PeerId]
|
|
46
|
-
}
|
|
47
|
-
}
|
|
47
|
+
this.#removeSocket(socket)
|
|
48
48
|
})
|
|
49
49
|
|
|
50
50
|
socket.on("message", message =>
|
|
@@ -58,136 +58,132 @@ export class NodeWSServerAdapter extends NetworkAdapter {
|
|
|
58
58
|
this.emit("ready", { network: this })
|
|
59
59
|
})
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (socket.isAlive
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
)
|
|
71
|
-
if (socket === otherSocket) {
|
|
72
|
-
this.emit("peer-disconnected", { peerId: otherPeerId as PeerId })
|
|
73
|
-
delete this.sockets[otherPeerId as PeerId]
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return socket.terminate()
|
|
61
|
+
const keepAliveId = setInterval(() => {
|
|
62
|
+
// Terminate connections to lost clients
|
|
63
|
+
const clients = this.server.clients as Set<WebSocketWithIsAlive>
|
|
64
|
+
clients.forEach(socket => {
|
|
65
|
+
if (socket.isAlive) {
|
|
66
|
+
// Mark all clients as potentially dead until we hear from them
|
|
67
|
+
socket.isAlive = false
|
|
68
|
+
socket.ping()
|
|
69
|
+
} else {
|
|
70
|
+
this.#terminate(socket)
|
|
77
71
|
}
|
|
78
|
-
socket.isAlive = false
|
|
79
|
-
socket.ping()
|
|
80
72
|
})
|
|
81
|
-
},
|
|
73
|
+
}, this.keepAliveInterval)
|
|
82
74
|
}
|
|
83
75
|
|
|
84
76
|
disconnect() {
|
|
85
|
-
|
|
77
|
+
const clients = this.server.clients as Set<WebSocketWithIsAlive>
|
|
78
|
+
clients.forEach(socket => {
|
|
79
|
+
this.#terminate(socket)
|
|
80
|
+
this.#removeSocket(socket)
|
|
81
|
+
})
|
|
86
82
|
}
|
|
87
83
|
|
|
88
84
|
send(message: FromServerMessage) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
85
|
+
assert("targetId" in message && message.targetId !== undefined)
|
|
86
|
+
if ("data" in message && message.data?.byteLength === 0)
|
|
87
|
+
throw new Error("Tried to send a zero-length message")
|
|
88
|
+
|
|
92
89
|
const senderId = this.peerId
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
90
|
+
assert(senderId, "No peerId set for the websocket server network adapter.")
|
|
91
|
+
|
|
92
|
+
const socket = this.sockets[message.targetId]
|
|
96
93
|
|
|
97
|
-
if (
|
|
98
|
-
log(`Tried to send
|
|
94
|
+
if (!socket) {
|
|
95
|
+
log(`Tried to send to disconnected peer: ${message.targetId}`)
|
|
99
96
|
return
|
|
100
97
|
}
|
|
101
98
|
|
|
102
99
|
const encoded = encode(message)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
encoded.byteOffset,
|
|
107
|
-
encoded.byteOffset + encoded.byteLength
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
this.sockets[message.targetId]?.send(arrayBuf)
|
|
100
|
+
const arrayBuf = toArrayBuffer(encoded)
|
|
101
|
+
|
|
102
|
+
socket.send(arrayBuf)
|
|
111
103
|
}
|
|
112
104
|
|
|
113
|
-
receiveMessage(
|
|
114
|
-
const
|
|
105
|
+
receiveMessage(messageBytes: Uint8Array, socket: WebSocket) {
|
|
106
|
+
const message: FromClientMessage = decode(messageBytes)
|
|
115
107
|
|
|
116
|
-
const { type, senderId } =
|
|
108
|
+
const { type, senderId } = message
|
|
117
109
|
|
|
118
110
|
const myPeerId = this.peerId
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
{
|
|
130
|
-
|
|
131
|
-
if (existingSocket) {
|
|
132
|
-
if (existingSocket.readyState === WebSocket.OPEN) {
|
|
133
|
-
existingSocket.close()
|
|
134
|
-
}
|
|
135
|
-
this.emit("peer-disconnected", { peerId: senderId })
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const { peerMetadata } = cbor
|
|
139
|
-
// Let the rest of the system know that we have a new connection.
|
|
140
|
-
this.emit("peer-candidate", {
|
|
141
|
-
peerId: senderId,
|
|
142
|
-
peerMetadata,
|
|
143
|
-
})
|
|
144
|
-
this.sockets[senderId] = socket
|
|
145
|
-
|
|
146
|
-
// In this client-server connection, there's only ever one peer: us!
|
|
147
|
-
// (and we pretend to be joined to every channel)
|
|
148
|
-
const selectedProtocolVersion = selectProtocol(
|
|
149
|
-
cbor.supportedProtocolVersions
|
|
150
|
-
)
|
|
151
|
-
if (selectedProtocolVersion === null) {
|
|
152
|
-
this.send({
|
|
153
|
-
type: "error",
|
|
154
|
-
senderId: this.peerId!,
|
|
155
|
-
message: "unsupported protocol version",
|
|
156
|
-
targetId: senderId,
|
|
157
|
-
})
|
|
158
|
-
this.sockets[senderId].close()
|
|
159
|
-
delete this.sockets[senderId]
|
|
160
|
-
} else {
|
|
161
|
-
this.send({
|
|
162
|
-
type: "peer",
|
|
163
|
-
senderId: this.peerId!,
|
|
164
|
-
peerMetadata: this.peerMetadata!,
|
|
165
|
-
selectedProtocolVersion: ProtocolV1,
|
|
166
|
-
targetId: senderId,
|
|
167
|
-
})
|
|
168
|
-
}
|
|
111
|
+
assert(myPeerId)
|
|
112
|
+
|
|
113
|
+
const documentId = "documentId" in message ? "@" + message.documentId : ""
|
|
114
|
+
const { byteLength } = messageBytes
|
|
115
|
+
log(`[${senderId}->${myPeerId}${documentId}] ${type} | ${byteLength} bytes`)
|
|
116
|
+
|
|
117
|
+
if (isJoinMessage(message)) {
|
|
118
|
+
const { peerMetadata, supportedProtocolVersions } = message
|
|
119
|
+
const existingSocket = this.sockets[senderId]
|
|
120
|
+
if (existingSocket) {
|
|
121
|
+
if (existingSocket.readyState === WebSocket.OPEN) {
|
|
122
|
+
existingSocket.close()
|
|
169
123
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
this.
|
|
180
|
-
|
|
124
|
+
this.emit("peer-disconnected", { peerId: senderId })
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Let the repo know that we have a new connection.
|
|
128
|
+
this.emit("peer-candidate", { peerId: senderId, peerMetadata })
|
|
129
|
+
this.sockets[senderId] = socket
|
|
130
|
+
|
|
131
|
+
const selectedProtocolVersion = selectProtocol(supportedProtocolVersions)
|
|
132
|
+
if (selectedProtocolVersion === null) {
|
|
133
|
+
this.send({
|
|
134
|
+
type: "error",
|
|
135
|
+
senderId: this.peerId!,
|
|
136
|
+
message: "unsupported protocol version",
|
|
137
|
+
targetId: senderId,
|
|
138
|
+
})
|
|
139
|
+
this.sockets[senderId].close()
|
|
140
|
+
delete this.sockets[senderId]
|
|
141
|
+
} else {
|
|
142
|
+
this.send({
|
|
143
|
+
type: "peer",
|
|
144
|
+
senderId: this.peerId!,
|
|
145
|
+
peerMetadata: this.peerMetadata!,
|
|
146
|
+
selectedProtocolVersion: ProtocolV1,
|
|
147
|
+
targetId: senderId,
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
} else if (isLeaveMessage(message)) {
|
|
151
|
+
const { senderId } = message
|
|
152
|
+
const socket = this.sockets[senderId]
|
|
153
|
+
/* c8 ignore next */
|
|
154
|
+
if (!socket) return
|
|
155
|
+
this.#terminate(socket as WebSocketWithIsAlive)
|
|
156
|
+
} else {
|
|
157
|
+
this.emit("message", message)
|
|
181
158
|
}
|
|
182
159
|
}
|
|
183
|
-
}
|
|
184
160
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
161
|
+
#terminate(socket: WebSocketWithIsAlive) {
|
|
162
|
+
this.#removeSocket(socket)
|
|
163
|
+
socket.terminate()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
#removeSocket(socket: WebSocketWithIsAlive) {
|
|
167
|
+
const peerId = this.#peerIdBySocket(socket)
|
|
168
|
+
if (!peerId) return
|
|
169
|
+
this.emit("peer-disconnected", { peerId })
|
|
170
|
+
delete this.sockets[peerId as PeerId]
|
|
188
171
|
}
|
|
189
|
-
|
|
190
|
-
|
|
172
|
+
|
|
173
|
+
#peerIdBySocket = (socket: WebSocket) => {
|
|
174
|
+
const isThisSocket = (peerId: string) =>
|
|
175
|
+
this.sockets[peerId as PeerId] === socket
|
|
176
|
+
const result = Object.keys(this.sockets).find(isThisSocket) as PeerId
|
|
177
|
+
return result ?? null
|
|
191
178
|
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const selectProtocol = (versions?: ProtocolVersion[]) => {
|
|
182
|
+
if (versions === undefined) return ProtocolV1
|
|
183
|
+
if (versions.includes(ProtocolV1)) return ProtocolV1
|
|
192
184
|
return null
|
|
193
185
|
}
|
|
186
|
+
|
|
187
|
+
interface WebSocketWithIsAlive extends WebSocket {
|
|
188
|
+
isAlive: boolean
|
|
189
|
+
}
|
package/src/assert.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/* c8 ignore start */
|
|
2
|
+
|
|
3
|
+
export function assert(value: boolean, message?: string): asserts value
|
|
4
|
+
export function assert<T>(
|
|
5
|
+
value: T | undefined,
|
|
6
|
+
message?: string
|
|
7
|
+
): asserts value is T
|
|
8
|
+
export function assert(value: any, message = "Assertion failed") {
|
|
9
|
+
if (value === false || value === null || value === undefined) {
|
|
10
|
+
const error = new Error(trimLines(message))
|
|
11
|
+
error.stack = removeLine(error.stack, "assert.ts")
|
|
12
|
+
throw error
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const trimLines = (s: string) =>
|
|
17
|
+
s
|
|
18
|
+
.split("\n")
|
|
19
|
+
.map(s => s.trim())
|
|
20
|
+
.join("\n")
|
|
21
|
+
|
|
22
|
+
const removeLine = (s = "", targetText: string) =>
|
|
23
|
+
s
|
|
24
|
+
.split("\n")
|
|
25
|
+
.filter(line => !line.includes(targetText))
|
|
26
|
+
.join("\n")
|
|
27
|
+
|
|
28
|
+
/* c8 ignore end */
|
package/src/messages.ts
CHANGED
|
@@ -46,11 +46,26 @@ export type ErrorMessage = {
|
|
|
46
46
|
targetId: PeerId
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
// This adapter doesn't use the network adapter Message types, it has its own idea of how to handle
|
|
50
|
-
// join/leave
|
|
51
|
-
|
|
52
49
|
/** A message from the client to the server */
|
|
53
50
|
export type FromClientMessage = JoinMessage | LeaveMessage | Message
|
|
54
51
|
|
|
55
52
|
/** A message from the server to the client */
|
|
56
53
|
export type FromServerMessage = PeerMessage | ErrorMessage | Message
|
|
54
|
+
|
|
55
|
+
// TYPE GUARDS
|
|
56
|
+
|
|
57
|
+
export const isJoinMessage = (
|
|
58
|
+
message: FromClientMessage
|
|
59
|
+
): message is JoinMessage => message.type === "join"
|
|
60
|
+
|
|
61
|
+
export const isLeaveMessage = (
|
|
62
|
+
message: FromClientMessage
|
|
63
|
+
): message is LeaveMessage => message.type === "leave"
|
|
64
|
+
|
|
65
|
+
export const isPeerMessage = (
|
|
66
|
+
message: FromServerMessage
|
|
67
|
+
): message is PeerMessage => message.type === "peer"
|
|
68
|
+
|
|
69
|
+
export const isErrorMessage = (
|
|
70
|
+
message: FromServerMessage
|
|
71
|
+
): message is ErrorMessage => message.type === "error"
|
|
@@ -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: Uint8Array) => {
|
|
6
|
+
const { buffer, byteOffset, byteLength } = bytes
|
|
7
|
+
return buffer.slice(byteOffset, byteOffset + byteLength)
|
|
8
|
+
}
|