@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.
@@ -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.19",
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": "^1.0.19",
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": "7d28ca50dfa437ac6f7b1722b89b3f6844b90de7"
34
+ "gitHead": "f4ce1376d900ad98f00a638626be9611077460b5"
34
35
  }
@@ -1,4 +1,9 @@
1
- import { NetworkAdapter, PeerId, cbor } from "@automerge/automerge-repo"
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
- const log = debug("WebsocketClient")
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
- // Type trickery required for platform independence,
21
- // see https://stackoverflow.com/questions/45802988/typescript-use-correct-version-of-settimeout-node-vs-window
22
- timerId?: ReturnType<typeof setTimeout>
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
- url: string
31
+ remotePeerId?: PeerId // this adapter only connects to one remote client at a time
27
32
 
28
- constructor(url: string) {
33
+ constructor(
34
+ public readonly url: string,
35
+ public readonly retryInterval = 5000
36
+ ) {
29
37
  super()
30
- this.url = url
38
+ this.#log = this.#log.extend(url)
31
39
  }
32
40
 
33
- connect(peerId: PeerId) {
34
- // If we're reconnecting make sure we remove the old event listeners
35
- // before creating a new connection.
36
- 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.
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
- // 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.
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(`@ ${this.url}: open`)
69
- clearInterval(this.timerId)
70
- this.timerId = undefined
71
- this.send(joinMessage(this.peerId!))
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(`${this.url}: close`)
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.timerId) {
83
- if (this.peerId) {
84
- this.connect(this.peerId)
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
- join() {
94
- if (!this.socket) {
95
- 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
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
- // The onOpen handler automatically sends a join message
125
+ // We'll try again in the `onOpen` handler
101
126
  }
102
127
  }
103
128
 
104
129
  disconnect() {
105
- if (!this.socket) {
106
- throw new Error("WTF, get a socket")
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.byteLength === 0) {
113
- throw new Error("tried to send a zero-length message")
114
- }
115
-
116
- if (!this.peerId) {
117
- throw new Error("Why don't we have a PeerID?")
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
- // This incantation deals with websocket sending the whole
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
- announceConnection(peerId: PeerId) {
136
- // return a peer object
137
- const myPeerId = this.peerId
138
- if (!myPeerId) {
139
- throw new Error("we should have a peer ID by now")
140
- }
141
- if (!this.#startupComplete) {
142
- this.#startupComplete = true
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(message: Uint8Array) {
150
- const decoded: FromServerMessage = cbor.decode(new Uint8Array(message))
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
- if (message.byteLength === 0) {
160
+ assert(this.socket)
161
+ if (messageBytes.byteLength === 0)
160
162
  throw new Error("received a zero-length message")
161
- }
162
163
 
163
- switch (type) {
164
- case "peer":
165
- log(`peer: ${senderId}`)
166
- this.announceConnection(senderId)
167
- break
168
- case "error":
169
- log(`error: ${decoded.message}`)
170
- break
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(senderId: PeerId): 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 { FromClientMessage, FromServerMessage } from "./messages.js"
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(server: WebSocketServer) {
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", function close() {
34
- clearInterval(interval)
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
- for (const [otherPeerId, otherSocket] of Object.entries(this.sockets)) {
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
- // Every interval, terminate connections to lost clients,
60
- // then mark all clients as potentially dead and then ping them.
61
- const interval = setInterval(() => {
62
- ;(this.server.clients as Set<WebSocketWithIsAlive>).forEach(socket => {
63
- if (socket.isAlive === false) {
64
- // Make sure we clean up this socket even though we're terminating.
65
- // This might be unnecessary but I have read reports of the close() not happening for 30s.
66
- for (const [otherPeerId, otherSocket] of Object.entries(
67
- this.sockets
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
- }, 5000)
73
+ }, this.keepAliveInterval)
80
74
  }
81
75
 
82
76
  disconnect() {
83
- // 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
+ })
84
82
  }
85
83
 
86
84
  send(message: FromServerMessage) {
87
- if ("data" in message && message.data.byteLength === 0) {
88
- throw new Error("tried to send a zero-length message")
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
- if (!senderId) {
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
- if (this.sockets[message.targetId] === undefined) {
96
- log(`Tried to send message to disconnected peer: ${message.targetId}`)
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
- // This incantation deals with websocket sending the whole
102
- // underlying buffer even if we just have a uint8array view on it
103
- const arrayBuf = encoded.buffer.slice(
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(message: Uint8Array, socket: WebSocket) {
112
- const cbor: FromClientMessage = decode(message)
105
+ receiveMessage(messageBytes: Uint8Array, socket: WebSocket) {
106
+ const message: FromClientMessage = decode(messageBytes)
113
107
 
114
- const { type, senderId } = cbor
108
+ const { type, senderId } = message
115
109
 
116
110
  const myPeerId = this.peerId
117
- if (!myPeerId) {
118
- throw new Error("Missing my peer ID.")
119
- }
120
- log(
121
- `[${senderId}->${myPeerId}${
122
- "documentId" in cbor ? "@" + cbor.documentId : ""
123
- }] ${type} | ${message.byteLength} bytes`
124
- )
125
- switch (type) {
126
- case "join":
127
- const existingSocket = this.sockets[senderId]
128
- if (existingSocket) {
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
- // Let the rest of the system know that we have a new connection.
136
- this.emit("peer-candidate", { peerId: senderId })
137
- this.sockets[senderId] = socket
138
-
139
- // In this client-server connection, there's only ever one peer: us!
140
- // (and we pretend to be joined to every channel)
141
- const selectedProtocolVersion = selectProtocol(
142
- cbor.supportedProtocolVersions
143
- )
144
- if (selectedProtocolVersion === null) {
145
- this.send({
146
- type: "error",
147
- senderId: this.peerId!,
148
- message: "unsupported protocol version",
149
- targetId: senderId,
150
- })
151
- this.sockets[senderId].close()
152
- delete this.sockets[senderId]
153
- } else {
154
- this.send({
155
- type: "peer",
156
- senderId: this.peerId!,
157
- selectedProtocolVersion: ProtocolV1,
158
- targetId: senderId,
159
- })
160
- }
161
- break
162
- case "leave":
163
- // It doesn't seem like this gets called;
164
- // we handle leaving in the socket close logic
165
- // TODO: confirm this
166
- // ?
167
- break
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
- function selectProtocol(versions?: ProtocolVersion[]): ProtocolVersion | null {
177
- if (versions === undefined) {
178
- return ProtocolV1
161
+ #terminate(socket: WebSocketWithIsAlive) {
162
+ this.#removeSocket(socket)
163
+ socket.terminate()
179
164
  }
180
- if (versions.includes(ProtocolV1)) {
181
- return ProtocolV1
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 */