@automerge/automerge-repo-network-websocket 1.1.0-alpha.7 → 1.1.1
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/README.md +77 -3
- 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 +6 -4
- 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 +119 -123
- 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
|
@@ -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
|
-
connect(peerId: PeerId, peerMetadata
|
|
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
|
+
}
|