@automerge/automerge-repo-network-websocket 1.1.0-alpha.6 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automerge/automerge-repo-network-websocket",
3
- "version": "1.1.0-alpha.6",
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": "^1.1.0-alpha.6",
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": "77ad0475a3b241a8efdf4e282fdffc8fed09b101"
34
+ "gitHead": "e9e7d3f27ec2ac8a2e9d122ece80598918940067"
34
35
  }
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  NetworkAdapter,
3
- type PeerId,
4
- type PeerMetadata,
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
- PeerMessage,
15
+ isErrorMessage,
16
+ isPeerMessage,
16
17
  } from "./messages.js"
17
18
  import { ProtocolV1 } from "./protocolVersion.js"
18
-
19
- const log = debug("WebsocketClient")
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
- // Type trickery required for platform independence,
27
- // see https://stackoverflow.com/questions/45802988/typescript-use-correct-version-of-settimeout-node-vs-window
28
- timerId?: ReturnType<typeof setTimeout>
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
- url: string
31
+ remotePeerId?: PeerId // this adapter only connects to one remote client at a time
33
32
 
34
- constructor(url: string) {
33
+ constructor(
34
+ public readonly url: string,
35
+ public readonly retryInterval = 5000
36
+ ) {
35
37
  super()
36
- this.url = url
38
+ this.#log = this.#log.extend(url)
37
39
  }
38
40
 
39
- connect(peerId: PeerId, peerMetadata: PeerMetadata) {
40
- // If we're reconnecting make sure we remove the old event listeners
41
- // before creating a new connection.
42
- if (this.socket) {
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
- // mark this adapter as ready if we haven't received an ack in 1 second.
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(`@ ${this.url}: open`)
76
- clearInterval(this.timerId)
77
- this.timerId = undefined
78
- this.send(joinMessage(this.peerId!, this.peerMetadata!))
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(`${this.url}: close`)
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.timerId) {
90
- if (this.peerId) {
91
- this.connect(this.peerId, this.peerMetadata!)
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
- join() {
101
- if (!this.socket) {
102
- throw new Error("WTF, get a socket")
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
- // The onOpen handler automatically sends a join message
125
+ // We'll try again in the `onOpen` handler
108
126
  }
109
127
  }
110
128
 
111
129
  disconnect() {
112
- if (!this.socket) {
113
- throw new Error("WTF, get a socket")
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.byteLength === 0) {
120
- throw new Error("tried to send a zero-length message")
121
- }
122
-
123
- if (!this.peerId) {
124
- throw new Error("Why don't we have a PeerID?")
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
- // This incantation deals with websocket sending the whole
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
- announceConnection(peerId: PeerId, peerMetadata: PeerMetadata) {
143
- // return a peer object
144
- const myPeerId = this.peerId
145
- if (!myPeerId) {
146
- throw new Error("we should have a peer ID by now")
147
- }
148
- if (!this.#startupComplete) {
149
- this.#startupComplete = true
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(message: Uint8Array) {
157
- const decoded: FromServerMessage = cbor.decode(new Uint8Array(message))
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
- if (message.byteLength === 0) {
160
+ assert(this.socket)
161
+ if (messageBytes.byteLength === 0)
167
162
  throw new Error("received a zero-length message")
168
- }
169
163
 
170
- switch (type) {
171
- case "peer": {
172
- const { peerMetadata } = decoded
173
- log(`peer: ${senderId}`)
174
- this.announceConnection(senderId, peerMetadata)
175
- break
176
- }
177
- case "error":
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 { FromClientMessage, FromServerMessage } from "./messages.js"
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(server: WebSocketServer) {
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", function close() {
36
- clearInterval(interval)
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
- for (const [otherPeerId, otherSocket] of Object.entries(this.sockets)) {
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
- // Every interval, terminate connections to lost clients,
62
- // then mark all clients as potentially dead and then ping them.
63
- const interval = setInterval(() => {
64
- ;(this.server.clients as Set<WebSocketWithIsAlive>).forEach(socket => {
65
- if (socket.isAlive === false) {
66
- // Make sure we clean up this socket even though we're terminating.
67
- // This might be unnecessary but I have read reports of the close() not happening for 30s.
68
- for (const [otherPeerId, otherSocket] of Object.entries(
69
- this.sockets
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
- }, 5000)
73
+ }, this.keepAliveInterval)
82
74
  }
83
75
 
84
76
  disconnect() {
85
- // throw new Error("The server doesn't join channels.")
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
- if ("data" in message && message.data.byteLength === 0) {
90
- throw new Error("tried to send a zero-length message")
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
- if (!senderId) {
94
- throw new Error("No peerId set for the websocket server network adapter.")
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 (this.sockets[message.targetId] === undefined) {
98
- log(`Tried to send message to disconnected peer: ${message.targetId}`)
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
- // This incantation deals with websocket sending the whole
104
- // underlying buffer even if we just have a uint8array view on it
105
- const arrayBuf = encoded.buffer.slice(
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(message: Uint8Array, socket: WebSocket) {
114
- const cbor: FromClientMessage = decode(message)
105
+ receiveMessage(messageBytes: Uint8Array, socket: WebSocket) {
106
+ const message: FromClientMessage = decode(messageBytes)
115
107
 
116
- const { type, senderId } = cbor
108
+ const { type, senderId } = message
117
109
 
118
110
  const myPeerId = this.peerId
119
- if (!myPeerId) {
120
- throw new Error("Missing my peer ID.")
121
- }
122
- log(
123
- `[${senderId}->${myPeerId}${
124
- "documentId" in cbor ? "@" + cbor.documentId : ""
125
- }] ${type} | ${message.byteLength} bytes`
126
- )
127
- switch (type) {
128
- case "join":
129
- {
130
- const existingSocket = this.sockets[senderId]
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
- break
171
- case "leave":
172
- // It doesn't seem like this gets called;
173
- // we handle leaving in the socket close logic
174
- // TODO: confirm this
175
- // ?
176
- break
177
-
178
- default:
179
- this.emit("message", cbor)
180
- 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)
181
158
  }
182
159
  }
183
- }
184
160
 
185
- function selectProtocol(versions?: ProtocolVersion[]): ProtocolVersion | null {
186
- if (versions === undefined) {
187
- return ProtocolV1
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
- if (versions.includes(ProtocolV1)) {
190
- return ProtocolV1
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
+ }