@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
|
@@ -7,49 +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
|
-
type StorageId,
|
|
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(
|
|
32
|
-
peerId: PeerId,
|
|
33
|
-
storageId: StorageId | undefined,
|
|
34
|
-
isEphemeral: boolean
|
|
35
|
-
) {
|
|
35
|
+
connect(peerId: PeerId, peerMetadata: PeerMetadata) {
|
|
36
36
|
this.peerId = peerId
|
|
37
|
-
this.
|
|
38
|
-
this.isEphemeral = isEphemeral
|
|
37
|
+
this.peerMetadata = peerMetadata
|
|
39
38
|
|
|
40
|
-
this.server.on("close",
|
|
41
|
-
clearInterval(
|
|
39
|
+
this.server.on("close", () => {
|
|
40
|
+
clearInterval(keepAliveId)
|
|
41
|
+
this.disconnect()
|
|
42
42
|
})
|
|
43
43
|
|
|
44
44
|
this.server.on("connection", (socket: WebSocketWithIsAlive) => {
|
|
45
45
|
// When a socket closes, or disconnects, remove it from our list
|
|
46
46
|
socket.on("close", () => {
|
|
47
|
-
|
|
48
|
-
if (socket === otherSocket) {
|
|
49
|
-
this.emit("peer-disconnected", { peerId: otherPeerId as PeerId })
|
|
50
|
-
delete this.sockets[otherPeerId as PeerId]
|
|
51
|
-
}
|
|
52
|
-
}
|
|
47
|
+
this.#removeSocket(socket)
|
|
53
48
|
})
|
|
54
49
|
|
|
55
50
|
socket.on("message", message =>
|
|
@@ -63,138 +58,132 @@ export class NodeWSServerAdapter extends NetworkAdapter {
|
|
|
63
58
|
this.emit("ready", { network: this })
|
|
64
59
|
})
|
|
65
60
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (socket.isAlive
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
)
|
|
76
|
-
if (socket === otherSocket) {
|
|
77
|
-
this.emit("peer-disconnected", { peerId: otherPeerId as PeerId })
|
|
78
|
-
delete this.sockets[otherPeerId as PeerId]
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
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)
|
|
82
71
|
}
|
|
83
|
-
socket.isAlive = false
|
|
84
|
-
socket.ping()
|
|
85
72
|
})
|
|
86
|
-
},
|
|
73
|
+
}, this.keepAliveInterval)
|
|
87
74
|
}
|
|
88
75
|
|
|
89
76
|
disconnect() {
|
|
90
|
-
|
|
77
|
+
const clients = this.server.clients as Set<WebSocketWithIsAlive>
|
|
78
|
+
clients.forEach(socket => {
|
|
79
|
+
this.#terminate(socket)
|
|
80
|
+
this.#removeSocket(socket)
|
|
81
|
+
})
|
|
91
82
|
}
|
|
92
83
|
|
|
93
84
|
send(message: FromServerMessage) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
+
|
|
97
89
|
const senderId = this.peerId
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
90
|
+
assert(senderId, "No peerId set for the websocket server network adapter.")
|
|
91
|
+
|
|
92
|
+
const socket = this.sockets[message.targetId]
|
|
101
93
|
|
|
102
|
-
if (
|
|
103
|
-
log(`Tried to send
|
|
94
|
+
if (!socket) {
|
|
95
|
+
log(`Tried to send to disconnected peer: ${message.targetId}`)
|
|
104
96
|
return
|
|
105
97
|
}
|
|
106
98
|
|
|
107
99
|
const encoded = encode(message)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
encoded.byteOffset,
|
|
112
|
-
encoded.byteOffset + encoded.byteLength
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
this.sockets[message.targetId]?.send(arrayBuf)
|
|
100
|
+
const arrayBuf = toArrayBuffer(encoded)
|
|
101
|
+
|
|
102
|
+
socket.send(arrayBuf)
|
|
116
103
|
}
|
|
117
104
|
|
|
118
|
-
receiveMessage(
|
|
119
|
-
const
|
|
105
|
+
receiveMessage(messageBytes: Uint8Array, socket: WebSocket) {
|
|
106
|
+
const message: FromClientMessage = decode(messageBytes)
|
|
120
107
|
|
|
121
|
-
const { type, senderId } =
|
|
108
|
+
const { type, senderId } = message
|
|
122
109
|
|
|
123
110
|
const myPeerId = this.peerId
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
{
|
|
135
|
-
|
|
136
|
-
if (existingSocket) {
|
|
137
|
-
if (existingSocket.readyState === WebSocket.OPEN) {
|
|
138
|
-
existingSocket.close()
|
|
139
|
-
}
|
|
140
|
-
this.emit("peer-disconnected", { peerId: senderId })
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const { storageId, isEphemeral } = cbor
|
|
144
|
-
// Let the rest of the system know that we have a new connection.
|
|
145
|
-
this.emit("peer-candidate", {
|
|
146
|
-
peerId: senderId,
|
|
147
|
-
storageId,
|
|
148
|
-
isEphemeral,
|
|
149
|
-
})
|
|
150
|
-
this.sockets[senderId] = socket
|
|
151
|
-
|
|
152
|
-
// In this client-server connection, there's only ever one peer: us!
|
|
153
|
-
// (and we pretend to be joined to every channel)
|
|
154
|
-
const selectedProtocolVersion = selectProtocol(
|
|
155
|
-
cbor.supportedProtocolVersions
|
|
156
|
-
)
|
|
157
|
-
if (selectedProtocolVersion === null) {
|
|
158
|
-
this.send({
|
|
159
|
-
type: "error",
|
|
160
|
-
senderId: this.peerId!,
|
|
161
|
-
message: "unsupported protocol version",
|
|
162
|
-
targetId: senderId,
|
|
163
|
-
})
|
|
164
|
-
this.sockets[senderId].close()
|
|
165
|
-
delete this.sockets[senderId]
|
|
166
|
-
} else {
|
|
167
|
-
this.send({
|
|
168
|
-
type: "peer",
|
|
169
|
-
senderId: this.peerId!,
|
|
170
|
-
storageId: this.storageId,
|
|
171
|
-
isEphemeral: this.isEphemeral,
|
|
172
|
-
selectedProtocolVersion: ProtocolV1,
|
|
173
|
-
targetId: senderId,
|
|
174
|
-
})
|
|
175
|
-
}
|
|
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()
|
|
176
123
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
this.
|
|
187
|
-
|
|
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)
|
|
188
158
|
}
|
|
189
159
|
}
|
|
190
|
-
}
|
|
191
160
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
161
|
+
#terminate(socket: WebSocketWithIsAlive) {
|
|
162
|
+
this.#removeSocket(socket)
|
|
163
|
+
socket.terminate()
|
|
195
164
|
}
|
|
196
|
-
|
|
197
|
-
|
|
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
|
|
198
178
|
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const selectProtocol = (versions?: ProtocolVersion[]) => {
|
|
182
|
+
if (versions === undefined) return ProtocolV1
|
|
183
|
+
if (versions.includes(ProtocolV1)) return ProtocolV1
|
|
199
184
|
return null
|
|
200
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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Message, PeerId,
|
|
1
|
+
import type { Message, PeerId, PeerMetadata } from "@automerge/automerge-repo"
|
|
2
2
|
import type { ProtocolVersion } from "./protocolVersion.js"
|
|
3
3
|
|
|
4
4
|
/** The sender is disconnecting */
|
|
@@ -13,12 +13,8 @@ export type JoinMessage = {
|
|
|
13
13
|
/** The PeerID of the client */
|
|
14
14
|
senderId: PeerId
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
/** Indicates whether other peers should persist the sync state of the sender peer.
|
|
20
|
-
* Sync state is only persisted for non-ephemeral peers */
|
|
21
|
-
isEphemeral: boolean
|
|
16
|
+
/** Metadata presented by the peer */
|
|
17
|
+
peerMetadata: PeerMetadata
|
|
22
18
|
|
|
23
19
|
/** The protocol version the client supports */
|
|
24
20
|
supportedProtocolVersions: ProtocolVersion[]
|
|
@@ -30,12 +26,8 @@ export type PeerMessage = {
|
|
|
30
26
|
/** The PeerID of the server */
|
|
31
27
|
senderId: PeerId
|
|
32
28
|
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
/** Indicates whether other peers should persist the sync state of the sender peer.
|
|
37
|
-
* Sync state is only persisted for non-ephemeral peers */
|
|
38
|
-
isEphemeral: boolean
|
|
29
|
+
/** Metadata presented by the peer */
|
|
30
|
+
peerMetadata: PeerMetadata
|
|
39
31
|
|
|
40
32
|
/** The protocol version the server selected for this connection */
|
|
41
33
|
selectedProtocolVersion: ProtocolVersion
|
|
@@ -54,11 +46,26 @@ export type ErrorMessage = {
|
|
|
54
46
|
targetId: PeerId
|
|
55
47
|
}
|
|
56
48
|
|
|
57
|
-
// This adapter doesn't use the network adapter Message types, it has its own idea of how to handle
|
|
58
|
-
// join/leave
|
|
59
|
-
|
|
60
49
|
/** A message from the client to the server */
|
|
61
50
|
export type FromClientMessage = JoinMessage | LeaveMessage | Message
|
|
62
51
|
|
|
63
52
|
/** A message from the server to the client */
|
|
64
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
|
+
}
|