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