@johnhenry/browsermesh-transport 0.1.1 → 0.1.2
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 +2 -2
- package/src/webrtc.mjs +163 -69
- package/src/websocket.mjs +27 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnhenry/browsermesh-transport",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "WebSocket, WebRTC, WebTransport, and relay adapters for BrowserMesh",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.mjs",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"@johnhenry/browsermesh-primitives": ">=0.0.1 <1.0.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"node-datachannel": "^0.33.
|
|
25
|
+
"node-datachannel": "^0.33.4"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"test": "node --import ./test/_setup-globals.mjs --test test/*.test.mjs",
|
package/src/webrtc.mjs
CHANGED
|
@@ -874,18 +874,32 @@ export class WebRTCPeerConnection {
|
|
|
874
874
|
// ---------------------------------------------------------------------------
|
|
875
875
|
|
|
876
876
|
/**
|
|
877
|
-
*
|
|
877
|
+
* Connection identifier used when a caller doesn't ask for a specific one.
|
|
878
|
+
* `connectToPeer(remotePodId)` (no `connectionId`) always resolves to this
|
|
879
|
+
* slot, so every pre-existing single-connection-per-peer call site keeps
|
|
880
|
+
* behaving exactly as before -- multiple independent connections to the
|
|
881
|
+
* same peer are strictly additive, opt-in via an explicit `connectionId`.
|
|
882
|
+
*/
|
|
883
|
+
export const DEFAULT_CONNECTION_ID = 'default'
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Manages multiple WebRTC peer connections indexed by remotePodId, and (see
|
|
887
|
+
* issue #116) optionally by a caller-chosen `connectionId` within a peer --
|
|
888
|
+
* e.g. one connection per traffic class, each paying its own ICE/STUN/DTLS
|
|
889
|
+
* negotiation cost independently. `connectionId` defaults to
|
|
890
|
+
* `DEFAULT_CONNECTION_ID`, so callers who never pass one see the original
|
|
891
|
+
* one-connection-per-peer behavior unchanged.
|
|
878
892
|
* Thin orchestration layer — signaling is left to the caller.
|
|
879
893
|
*/
|
|
880
894
|
export class WebRTCMeshManager {
|
|
881
895
|
#localPodId
|
|
882
896
|
#iceServers
|
|
883
|
-
#connections = new Map() // remotePodId -> WebRTCPeerConnection
|
|
897
|
+
#connections = new Map() // remotePodId -> Map<connectionId, WebRTCPeerConnection>
|
|
884
898
|
#onLog
|
|
885
899
|
#messageCbs = []
|
|
886
900
|
#reconnectOfferCbs = []
|
|
887
|
-
#reconnectAttempts = new Map() // remotePodId -> count
|
|
888
|
-
#reconnectTimers = new Map() // remotePodId -> timer handle
|
|
901
|
+
#reconnectAttempts = new Map() // "remotePodId connectionId" -> count
|
|
902
|
+
#reconnectTimers = new Map() // "remotePodId connectionId" -> timer handle
|
|
889
903
|
#maxReconnectAttempts
|
|
890
904
|
#reconnectBaseDelayMs
|
|
891
905
|
#disconnectedGraceMs
|
|
@@ -918,13 +932,17 @@ export class WebRTCMeshManager {
|
|
|
918
932
|
/** Local pod identifier. */
|
|
919
933
|
get localPodId() { return this.#localPodId }
|
|
920
934
|
|
|
921
|
-
/** Number of tracked connections. */
|
|
922
|
-
get connectionCount() {
|
|
935
|
+
/** Number of tracked connections, across all peers and connectionIds. */
|
|
936
|
+
get connectionCount() {
|
|
937
|
+
let count = 0
|
|
938
|
+
for (const byConnectionId of this.#connections.values()) count += byConnectionId.size
|
|
939
|
+
return count
|
|
940
|
+
}
|
|
923
941
|
|
|
924
942
|
/**
|
|
925
943
|
* Register a global message listener that fires for all connections.
|
|
926
944
|
*
|
|
927
|
-
* @param {Function} cb - Called with (data, remotePodId)
|
|
945
|
+
* @param {Function} cb - Called with (data, remotePodId, connectionId)
|
|
928
946
|
*/
|
|
929
947
|
onMessage(cb) { this.#messageCbs.push(cb) }
|
|
930
948
|
|
|
@@ -934,20 +952,40 @@ export class WebRTCMeshManager {
|
|
|
934
952
|
* this offer through the same external signaling channel used for the
|
|
935
953
|
* original connection.
|
|
936
954
|
*
|
|
937
|
-
* @param {Function} cb - Called with (offer: {type, sdp}, remotePodId: string)
|
|
955
|
+
* @param {Function} cb - Called with (offer: {type, sdp}, remotePodId: string, connectionId: string)
|
|
938
956
|
*/
|
|
939
957
|
onReconnectOffer(cb) { this.#reconnectOfferCbs.push(cb) }
|
|
940
958
|
|
|
959
|
+
#reconnectKey(remotePodId, connectionId) { return `${remotePodId} ${connectionId}` }
|
|
960
|
+
|
|
941
961
|
/**
|
|
942
962
|
* Create or return an existing WebRTCPeerConnection for a remote pod.
|
|
943
|
-
* Returns the same instance on duplicate calls with the same remotePodId
|
|
963
|
+
* Returns the same instance on duplicate calls with the same remotePodId
|
|
964
|
+
* *and* connectionId.
|
|
965
|
+
*
|
|
966
|
+
* Passing a `connectionId` other than the default opens a fully
|
|
967
|
+
* independent `RTCPeerConnection` to the same peer -- its own ICE/STUN/
|
|
968
|
+
* DTLS negotiation, its own DataChannel, its own reconnect backoff. See
|
|
969
|
+
* issue #116: this is the real cost of the feature, not a limitation of
|
|
970
|
+
* this method -- there is no way to get a second logical channel to a
|
|
971
|
+
* peer more cheaply than that without reusing an existing connection (see
|
|
972
|
+
* `WebRTCPeerConnection`'s single-DataChannel model / issue #115).
|
|
944
973
|
*
|
|
945
974
|
* @param {string} remotePodId
|
|
975
|
+
* @param {object} [opts]
|
|
976
|
+
* @param {string} [opts.connectionId] - Defaults to `DEFAULT_CONNECTION_ID`.
|
|
977
|
+
* Independent connectionIds to the same remotePodId are independent
|
|
978
|
+
* `RTCPeerConnection`s.
|
|
946
979
|
* @returns {Promise<WebRTCPeerConnection>}
|
|
947
980
|
*/
|
|
948
|
-
async connectToPeer(remotePodId) {
|
|
949
|
-
|
|
950
|
-
|
|
981
|
+
async connectToPeer(remotePodId, { connectionId = DEFAULT_CONNECTION_ID } = {}) {
|
|
982
|
+
let byConnectionId = this.#connections.get(remotePodId)
|
|
983
|
+
if (byConnectionId?.has(connectionId)) {
|
|
984
|
+
return byConnectionId.get(connectionId)
|
|
985
|
+
}
|
|
986
|
+
if (!byConnectionId) {
|
|
987
|
+
byConnectionId = new Map()
|
|
988
|
+
this.#connections.set(remotePodId, byConnectionId)
|
|
951
989
|
}
|
|
952
990
|
const conn = new WebRTCPeerConnection({
|
|
953
991
|
localPodId: this.#localPodId,
|
|
@@ -959,21 +997,25 @@ export class WebRTCMeshManager {
|
|
|
959
997
|
// Forward messages to manager-level listeners
|
|
960
998
|
conn.onMessage((data) => {
|
|
961
999
|
for (const cb of this.#messageCbs) {
|
|
962
|
-
try { cb(data, remotePodId) } catch (e) { silentCatch('clawser-mesh-webrtc', 'swallow', e) }
|
|
1000
|
+
try { cb(data, remotePodId, connectionId) } catch (e) { silentCatch('clawser-mesh-webrtc', 'swallow', e) }
|
|
963
1001
|
}
|
|
964
1002
|
})
|
|
965
1003
|
// Auto-remove on close
|
|
966
1004
|
conn.onClose(() => {
|
|
967
|
-
this.#connections.
|
|
968
|
-
|
|
1005
|
+
const stillTracked = this.#connections.get(remotePodId)
|
|
1006
|
+
if (stillTracked) {
|
|
1007
|
+
stillTracked.delete(connectionId)
|
|
1008
|
+
if (stillTracked.size === 0) this.#connections.delete(remotePodId)
|
|
1009
|
+
}
|
|
1010
|
+
this.#clearReconnectState(remotePodId, connectionId)
|
|
969
1011
|
})
|
|
970
1012
|
// Reset backoff once the connection actually recovers
|
|
971
1013
|
conn.onStateChange((state) => {
|
|
972
|
-
if (state === 'connected') this.#clearReconnectState(remotePodId)
|
|
1014
|
+
if (state === 'connected') this.#clearReconnectState(remotePodId, connectionId)
|
|
973
1015
|
})
|
|
974
1016
|
// Auto-retry with exponential backoff on failure/disconnect
|
|
975
|
-
conn.onError(() => this.#scheduleReconnect(remotePodId, conn))
|
|
976
|
-
|
|
1017
|
+
conn.onError(() => this.#scheduleReconnect(remotePodId, connectionId, conn))
|
|
1018
|
+
byConnectionId.set(connectionId, conn)
|
|
977
1019
|
return conn
|
|
978
1020
|
}
|
|
979
1021
|
|
|
@@ -984,108 +1026,135 @@ export class WebRTCMeshManager {
|
|
|
984
1026
|
* @param {object} [opts]
|
|
985
1027
|
* @param {boolean} [opts.force=false] - Renegotiate even if the connection
|
|
986
1028
|
* looks healthy. Without it a healthy connection is left alone.
|
|
1029
|
+
* @param {string} [opts.connectionId] - Defaults to `DEFAULT_CONNECTION_ID`.
|
|
987
1030
|
* @returns {Promise<{type: 'offer', sdp: string, renegotiation: true}|null>}
|
|
988
1031
|
* null if there is no such connection, or nothing to repair.
|
|
989
1032
|
*/
|
|
990
|
-
async reconnectPeer(remotePodId, { force = false } = {}) {
|
|
991
|
-
const conn = this.#connections.get(remotePodId)
|
|
1033
|
+
async reconnectPeer(remotePodId, { force = false, connectionId = DEFAULT_CONNECTION_ID } = {}) {
|
|
1034
|
+
const conn = this.#connections.get(remotePodId)?.get(connectionId)
|
|
992
1035
|
if (!conn) return null
|
|
993
1036
|
const offer = await conn.reconnect({ force })
|
|
994
1037
|
if (!offer) return null
|
|
995
|
-
this.#notifyReconnectOffer(offer, remotePodId)
|
|
1038
|
+
this.#notifyReconnectOffer(offer, remotePodId, connectionId)
|
|
996
1039
|
return offer
|
|
997
1040
|
}
|
|
998
1041
|
|
|
999
|
-
#clearReconnectState(remotePodId) {
|
|
1000
|
-
this.#
|
|
1001
|
-
|
|
1042
|
+
#clearReconnectState(remotePodId, connectionId) {
|
|
1043
|
+
const key = this.#reconnectKey(remotePodId, connectionId)
|
|
1044
|
+
this.#reconnectAttempts.delete(key)
|
|
1045
|
+
const timer = this.#reconnectTimers.get(key)
|
|
1002
1046
|
if (timer) {
|
|
1003
1047
|
clearTimeout(timer)
|
|
1004
|
-
this.#reconnectTimers.delete(
|
|
1048
|
+
this.#reconnectTimers.delete(key)
|
|
1005
1049
|
}
|
|
1006
1050
|
}
|
|
1007
1051
|
|
|
1008
|
-
#notifyReconnectOffer(offer, remotePodId) {
|
|
1052
|
+
#notifyReconnectOffer(offer, remotePodId, connectionId) {
|
|
1009
1053
|
for (const cb of this.#reconnectOfferCbs) {
|
|
1010
|
-
try { cb(offer, remotePodId) } catch (e) { silentCatch('clawser-mesh-webrtc', 'swallow', e) }
|
|
1054
|
+
try { cb(offer, remotePodId, connectionId) } catch (e) { silentCatch('clawser-mesh-webrtc', 'swallow', e) }
|
|
1011
1055
|
}
|
|
1012
1056
|
}
|
|
1013
1057
|
|
|
1014
|
-
#scheduleReconnect(remotePodId, conn) {
|
|
1015
|
-
|
|
1016
|
-
|
|
1058
|
+
#scheduleReconnect(remotePodId, connectionId, conn) {
|
|
1059
|
+
const key = this.#reconnectKey(remotePodId, connectionId)
|
|
1060
|
+
if (this.#reconnectTimers.has(key)) return // already scheduled
|
|
1061
|
+
const attempts = this.#reconnectAttempts.get(key) || 0
|
|
1017
1062
|
if (attempts >= this.#maxReconnectAttempts) {
|
|
1018
|
-
if (this.#onLog) this.#onLog(`Giving up reconnecting to ${remotePodId} after ${attempts} attempts`)
|
|
1063
|
+
if (this.#onLog) this.#onLog(`Giving up reconnecting to ${remotePodId} (${connectionId}) after ${attempts} attempts`)
|
|
1019
1064
|
return
|
|
1020
1065
|
}
|
|
1021
1066
|
const delay = this.#reconnectBaseDelayMs * (2 ** attempts)
|
|
1022
|
-
this.#reconnectAttempts.set(
|
|
1067
|
+
this.#reconnectAttempts.set(key, attempts + 1)
|
|
1023
1068
|
const timer = setTimeout(async () => {
|
|
1024
|
-
this.#reconnectTimers.delete(
|
|
1025
|
-
if (!this.#connections.
|
|
1069
|
+
this.#reconnectTimers.delete(key)
|
|
1070
|
+
if (!this.#connections.get(remotePodId)?.has(connectionId)) return // closed/removed meanwhile
|
|
1026
1071
|
try {
|
|
1027
1072
|
const offer = await conn.reconnect()
|
|
1028
1073
|
if (offer) {
|
|
1029
|
-
this.#notifyReconnectOffer(offer, remotePodId)
|
|
1074
|
+
this.#notifyReconnectOffer(offer, remotePodId, connectionId)
|
|
1030
1075
|
} else {
|
|
1031
1076
|
// Nothing was wrong with the connection after all -- the error that
|
|
1032
1077
|
// scheduled this attempt was spurious, or it healed while we waited.
|
|
1033
1078
|
// Refund the backoff rather than counting it against the peer.
|
|
1034
|
-
this.#clearReconnectState(remotePodId)
|
|
1079
|
+
this.#clearReconnectState(remotePodId, connectionId)
|
|
1035
1080
|
}
|
|
1036
1081
|
} catch (e) { silentCatch('clawser-mesh-webrtc', 'reconnect-attempt', e) }
|
|
1037
1082
|
}, delay)
|
|
1038
|
-
this.#reconnectTimers.set(
|
|
1083
|
+
this.#reconnectTimers.set(key, timer)
|
|
1039
1084
|
}
|
|
1040
1085
|
|
|
1041
1086
|
/**
|
|
1042
|
-
* Get an existing connection by remotePodId.
|
|
1087
|
+
* Get an existing connection by remotePodId (and, optionally, connectionId).
|
|
1043
1088
|
*
|
|
1044
1089
|
* @param {string} remotePodId
|
|
1090
|
+
* @param {string} [connectionId] - Defaults to `DEFAULT_CONNECTION_ID`.
|
|
1045
1091
|
* @returns {WebRTCPeerConnection|null}
|
|
1046
1092
|
*/
|
|
1047
|
-
getConnection(remotePodId) {
|
|
1048
|
-
return this.#connections.get(remotePodId) || null
|
|
1093
|
+
getConnection(remotePodId, connectionId = DEFAULT_CONNECTION_ID) {
|
|
1094
|
+
return this.#connections.get(remotePodId)?.get(connectionId) || null
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* List every connectionId tracked for a peer, alongside its connection.
|
|
1099
|
+
* Empty array if the peer has no tracked connections at all.
|
|
1100
|
+
*
|
|
1101
|
+
* @param {string} remotePodId
|
|
1102
|
+
* @returns {Array<{connectionId: string, connection: WebRTCPeerConnection}>}
|
|
1103
|
+
*/
|
|
1104
|
+
getConnectionsFor(remotePodId) {
|
|
1105
|
+
const byConnectionId = this.#connections.get(remotePodId)
|
|
1106
|
+
if (!byConnectionId) return []
|
|
1107
|
+
return [...byConnectionId.entries()].map(([connectionId, connection]) => ({ connectionId, connection }))
|
|
1049
1108
|
}
|
|
1050
1109
|
|
|
1051
1110
|
/**
|
|
1052
1111
|
* Check whether a connection to remotePodId exists.
|
|
1053
1112
|
*
|
|
1054
1113
|
* @param {string} remotePodId
|
|
1114
|
+
* @param {string} [connectionId] - If given, checks that exact connection.
|
|
1115
|
+
* If omitted, checks whether *any* connection to remotePodId exists
|
|
1116
|
+
* (matches the pre-#116 behavior for single-connection-per-peer callers).
|
|
1055
1117
|
* @returns {boolean}
|
|
1056
1118
|
*/
|
|
1057
|
-
hasConnection(remotePodId) {
|
|
1058
|
-
|
|
1119
|
+
hasConnection(remotePodId, connectionId) {
|
|
1120
|
+
const byConnectionId = this.#connections.get(remotePodId)
|
|
1121
|
+
if (!byConnectionId) return false
|
|
1122
|
+
return connectionId === undefined ? byConnectionId.size > 0 : byConnectionId.has(connectionId)
|
|
1059
1123
|
}
|
|
1060
1124
|
|
|
1061
1125
|
/**
|
|
1062
1126
|
* List all tracked connections with their current state.
|
|
1063
1127
|
*
|
|
1064
|
-
* @returns {Array<{remotePodId: string, state: string}>}
|
|
1128
|
+
* @returns {Array<{remotePodId: string, connectionId: string, state: string}>}
|
|
1065
1129
|
*/
|
|
1066
1130
|
listConnections() {
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1131
|
+
const results = []
|
|
1132
|
+
for (const [remotePodId, byConnectionId] of this.#connections.entries()) {
|
|
1133
|
+
for (const [connectionId, conn] of byConnectionId.entries()) {
|
|
1134
|
+
results.push({ remotePodId, connectionId, state: conn.state })
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
return results
|
|
1071
1138
|
}
|
|
1072
1139
|
|
|
1073
1140
|
/**
|
|
1074
|
-
* Query `getConnectionStats()` on every tracked connection
|
|
1075
|
-
*
|
|
1076
|
-
* the rest — its entry carries
|
|
1077
|
-
* `lastStats` for synchronous readers
|
|
1078
|
-
* which can't await this method).
|
|
1141
|
+
* Query `getConnectionStats()` on every tracked connection (every
|
|
1142
|
+
* connectionId, for every peer). A single connection's stats query
|
|
1143
|
+
* failing (e.g. mid-teardown) doesn't abort the rest — its entry carries
|
|
1144
|
+
* `error` instead. Result is cached on `lastStats` for synchronous readers
|
|
1145
|
+
* (e.g. MeshInspector.snapshot(), which can't await this method).
|
|
1079
1146
|
*
|
|
1080
1147
|
* @returns {Promise<Array<object>>}
|
|
1081
1148
|
*/
|
|
1082
1149
|
async getAllConnectionStats() {
|
|
1083
1150
|
const results = []
|
|
1084
|
-
for (const [remotePodId,
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1151
|
+
for (const [remotePodId, byConnectionId] of this.#connections.entries()) {
|
|
1152
|
+
for (const [connectionId, conn] of byConnectionId.entries()) {
|
|
1153
|
+
try {
|
|
1154
|
+
results.push({ ...(await conn.getConnectionStats()), connectionId })
|
|
1155
|
+
} catch (err) {
|
|
1156
|
+
results.push({ remotePodId, connectionId, state: conn.state, error: err?.message || String(err) })
|
|
1157
|
+
}
|
|
1089
1158
|
}
|
|
1090
1159
|
}
|
|
1091
1160
|
this.#lastStats = results
|
|
@@ -1100,15 +1169,28 @@ export class WebRTCMeshManager {
|
|
|
1100
1169
|
get lastStats() { return this.#lastStats }
|
|
1101
1170
|
|
|
1102
1171
|
/**
|
|
1103
|
-
* Broadcast data to all connected peers
|
|
1172
|
+
* Broadcast data to all connected peers, once per peer -- even if a peer
|
|
1173
|
+
* has multiple tracked connections (issue #116), it gets the message once,
|
|
1174
|
+
* over its default connection if that one is open, otherwise over
|
|
1175
|
+
* whichever of its other connections is. Callers that need to reach every
|
|
1176
|
+
* connection of every peer explicitly (rather than one message per peer)
|
|
1177
|
+
* should iterate `getConnectionsFor()`/`listConnections()` themselves.
|
|
1104
1178
|
*
|
|
1105
1179
|
* @param {string|object} data
|
|
1106
1180
|
* @returns {number} Number of peers the message was sent to
|
|
1107
1181
|
*/
|
|
1108
1182
|
broadcast(data) {
|
|
1109
1183
|
let sent = 0
|
|
1110
|
-
for (const
|
|
1111
|
-
|
|
1184
|
+
for (const byConnectionId of this.#connections.values()) {
|
|
1185
|
+
const defaultConn = byConnectionId.get(DEFAULT_CONNECTION_ID)
|
|
1186
|
+
// A Map entry is truthy regardless of isOpen, so this cannot be
|
|
1187
|
+
// `defaultConn || [...].find(isOpen)` -- that would always short-
|
|
1188
|
+
// circuit on the default entry existing at all, open or not, and
|
|
1189
|
+
// never fall back.
|
|
1190
|
+
const conn = (defaultConn && defaultConn.isOpen)
|
|
1191
|
+
? defaultConn
|
|
1192
|
+
: [...byConnectionId.values()].find((c) => c.isOpen)
|
|
1193
|
+
if (conn && conn.isOpen) {
|
|
1112
1194
|
try {
|
|
1113
1195
|
conn.send(data)
|
|
1114
1196
|
sent++
|
|
@@ -1122,22 +1204,34 @@ export class WebRTCMeshManager {
|
|
|
1122
1204
|
* Close a specific peer connection.
|
|
1123
1205
|
*
|
|
1124
1206
|
* @param {string} remotePodId
|
|
1125
|
-
* @
|
|
1207
|
+
* @param {string} [connectionId] - If given, closes just that connection.
|
|
1208
|
+
* If omitted, closes *every* tracked connection to remotePodId (matches
|
|
1209
|
+
* the pre-#116 behavior for single-connection-per-peer callers).
|
|
1210
|
+
* @returns {boolean} True if at least one connection was found and closed
|
|
1126
1211
|
*/
|
|
1127
|
-
closePeer(remotePodId) {
|
|
1128
|
-
const
|
|
1129
|
-
if (!
|
|
1130
|
-
|
|
1131
|
-
|
|
1212
|
+
closePeer(remotePodId, connectionId) {
|
|
1213
|
+
const byConnectionId = this.#connections.get(remotePodId)
|
|
1214
|
+
if (!byConnectionId || byConnectionId.size === 0) return false
|
|
1215
|
+
if (connectionId !== undefined) {
|
|
1216
|
+
const conn = byConnectionId.get(connectionId)
|
|
1217
|
+
if (!conn) return false
|
|
1218
|
+
conn.close()
|
|
1219
|
+
// conn.onClose() (wired in connectToPeer) removes it from #connections.
|
|
1220
|
+
return true
|
|
1221
|
+
}
|
|
1222
|
+
for (const conn of [...byConnectionId.values()]) conn.close()
|
|
1132
1223
|
return true
|
|
1133
1224
|
}
|
|
1134
1225
|
|
|
1135
1226
|
/**
|
|
1136
|
-
* Close all peer connections
|
|
1227
|
+
* Close all peer connections (every connectionId, for every peer) and
|
|
1228
|
+
* clear internal state.
|
|
1137
1229
|
*/
|
|
1138
1230
|
closeAll() {
|
|
1139
|
-
for (const
|
|
1140
|
-
|
|
1231
|
+
for (const byConnectionId of this.#connections.values()) {
|
|
1232
|
+
for (const conn of byConnectionId.values()) {
|
|
1233
|
+
try { conn.close() } catch (e) { silentCatch('clawser-mesh-webrtc', 'conn.close', e) }
|
|
1234
|
+
}
|
|
1141
1235
|
}
|
|
1142
1236
|
this.#connections.clear()
|
|
1143
1237
|
}
|
package/src/websocket.mjs
CHANGED
|
@@ -492,7 +492,7 @@ export class WebRTCTransport {
|
|
|
492
492
|
});
|
|
493
493
|
|
|
494
494
|
// Set up signaler listeners for remote ICE candidates
|
|
495
|
-
this.#signaler.onIceCandidate((
|
|
495
|
+
this.#signaler.onIceCandidate((data) => {
|
|
496
496
|
if (this.#pc) {
|
|
497
497
|
// The promise must not be dropped. addIceCandidate rejects on a
|
|
498
498
|
// malformed candidate and on one that arrives before the remote
|
|
@@ -500,7 +500,7 @@ export class WebRTCTransport {
|
|
|
500
500
|
// unhandled rejection ends a Node process by default. webrtc.mjs's
|
|
501
501
|
// addIceCandidate docblock records this being fixed there; this path
|
|
502
502
|
// still had it.
|
|
503
|
-
Promise.resolve(this.#pc.addIceCandidate(candidate)).catch((e) => {
|
|
503
|
+
Promise.resolve(this.#pc.addIceCandidate(data.candidate)).catch((e) => {
|
|
504
504
|
silentCatch('clawser-mesh-websocket', 'ignore-rejected-ice-candidate', e);
|
|
505
505
|
});
|
|
506
506
|
}
|
|
@@ -520,10 +520,10 @@ export class WebRTCTransport {
|
|
|
520
520
|
reject(new Error('WebRTC answer timeout'));
|
|
521
521
|
}, 30000);
|
|
522
522
|
|
|
523
|
-
this.#signaler.onAnswer(async (
|
|
523
|
+
this.#signaler.onAnswer(async (data) => {
|
|
524
524
|
clearTimeout(timeout);
|
|
525
525
|
try {
|
|
526
|
-
await this.#pc.setRemoteDescription(answer);
|
|
526
|
+
await this.#pc.setRemoteDescription(data.answer);
|
|
527
527
|
} catch (err) {
|
|
528
528
|
this.#state = 'disconnected';
|
|
529
529
|
reject(err);
|
|
@@ -563,14 +563,29 @@ export class WebRTCTransport {
|
|
|
563
563
|
await this.#pc.setLocalDescription(answer);
|
|
564
564
|
await this.#signaler.sendAnswer(this.#remotePodId, answer);
|
|
565
565
|
|
|
566
|
-
// The data channel will arrive via ondatachannel event
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
566
|
+
// The data channel will arrive via the ondatachannel event, and callers
|
|
567
|
+
// (e.g. HandshakeCoordinator.acceptConnection) await this method
|
|
568
|
+
// expecting the connection to be ready once it resolves -- so, like
|
|
569
|
+
// connect(), this must not resolve until the data channel is actually
|
|
570
|
+
// open, not merely once it has arrived or once the answer was sent.
|
|
571
|
+
return new Promise((resolve) => {
|
|
572
|
+
this.#pc.addEventListener('datachannel', (ev) => {
|
|
573
|
+
this.#dataChannel = ev.channel;
|
|
574
|
+
this._attachDataChannelListeners(this.#dataChannel);
|
|
575
|
+
if (this.#dataChannel.readyState === 'open') {
|
|
576
|
+
this.#state = 'connected';
|
|
577
|
+
this._fireEvent('open');
|
|
578
|
+
resolve();
|
|
579
|
+
} else {
|
|
580
|
+
const onDCOpen = () => {
|
|
581
|
+
this.#dataChannel.removeEventListener('open', onDCOpen);
|
|
582
|
+
this.#state = 'connected';
|
|
583
|
+
this._fireEvent('open');
|
|
584
|
+
resolve();
|
|
585
|
+
};
|
|
586
|
+
this.#dataChannel.addEventListener('open', onDCOpen);
|
|
587
|
+
}
|
|
588
|
+
});
|
|
574
589
|
});
|
|
575
590
|
}
|
|
576
591
|
|