@fairfox/polly 0.54.0 → 0.56.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/dist/src/mesh.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export type { MeshSignalingClientOptions, SignalingMessage as MeshSignalingMessa
|
|
|
29
29
|
export { MeshSignalingClient } from "./shared/lib/mesh-signaling-client";
|
|
30
30
|
export type { MeshStateOptions } from "./shared/lib/mesh-state";
|
|
31
31
|
export { $meshCounter, $meshList, $meshState, $meshText, configureMeshState, resetMeshState, } from "./shared/lib/mesh-state";
|
|
32
|
-
export type { MeshWebRTCAdapterOptions } from "./shared/lib/mesh-webrtc-adapter";
|
|
32
|
+
export type { InFlightSyncSnapshot, MeshWebRTCAdapterOptions, SlotInitiationDecision, SlotInitiationRejectionReason, SweepSnapshot, SyncHandshakeAttemptSnapshot, SyncProgressEvent, TransportSnapshot, } from "./shared/lib/mesh-webrtc-adapter";
|
|
33
33
|
export { DEFAULT_ICE_SERVERS, MeshWebRTCAdapter } from "./shared/lib/mesh-webrtc-adapter";
|
|
34
34
|
export type { CreatePairingTokenOptions, PairingToken, } from "./shared/lib/pairing";
|
|
35
35
|
export { applyPairingToken, createPairingToken, createPairingTokenWithFreshIdentity, DEFAULT_PAIRING_TTL_MS, decodePairingToken, encodePairingToken, isPairingTokenExpired, PAIRING_NONCE_BYTES, PAIRING_TOKEN_VERSION, PairingError, parsePairingToken, serialisePairingToken, } from "./shared/lib/pairing";
|
package/dist/src/mesh.js
CHANGED
|
@@ -1933,6 +1933,141 @@ function findSubarray2(haystack, needle) {
|
|
|
1933
1933
|
}
|
|
1934
1934
|
|
|
1935
1935
|
// src/shared/lib/mesh-webrtc-adapter.ts
|
|
1936
|
+
async function collectTransportSnapshot(connection, lastDataChannelError) {
|
|
1937
|
+
const at = performance.now();
|
|
1938
|
+
let report;
|
|
1939
|
+
try {
|
|
1940
|
+
report = await connection.getStats();
|
|
1941
|
+
} catch {
|
|
1942
|
+
return emptyTransportSnapshot(lastDataChannelError, at);
|
|
1943
|
+
}
|
|
1944
|
+
const parsed = partitionStats(report);
|
|
1945
|
+
const selectedPair = selectActivePair(parsed);
|
|
1946
|
+
const selectedCandidatePair = selectedPair ? buildCandidatePairView(selectedPair, parsed.localCands, parsed.remoteCands) : undefined;
|
|
1947
|
+
return {
|
|
1948
|
+
selectedCandidatePair,
|
|
1949
|
+
retransmittedPacketsSent: parsed.retransmittedPacketsSent,
|
|
1950
|
+
retransmittedBytesSent: parsed.retransmittedBytesSent,
|
|
1951
|
+
lastDataChannelError,
|
|
1952
|
+
at
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
function emptyTransportSnapshot(lastDataChannelError, at) {
|
|
1956
|
+
return {
|
|
1957
|
+
selectedCandidatePair: undefined,
|
|
1958
|
+
retransmittedPacketsSent: undefined,
|
|
1959
|
+
retransmittedBytesSent: undefined,
|
|
1960
|
+
lastDataChannelError,
|
|
1961
|
+
at
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
function partitionStats(report) {
|
|
1965
|
+
const out = {
|
|
1966
|
+
localCands: new Map,
|
|
1967
|
+
remoteCands: new Map,
|
|
1968
|
+
pairs: new Map,
|
|
1969
|
+
selectedPairId: undefined,
|
|
1970
|
+
retransmittedPacketsSent: undefined,
|
|
1971
|
+
retransmittedBytesSent: undefined
|
|
1972
|
+
};
|
|
1973
|
+
const iter = report.values?.() ?? [];
|
|
1974
|
+
for (const raw of iter) {
|
|
1975
|
+
if (!raw || typeof raw !== "object")
|
|
1976
|
+
continue;
|
|
1977
|
+
const stat = raw;
|
|
1978
|
+
ingestStat(stat, out);
|
|
1979
|
+
}
|
|
1980
|
+
return out;
|
|
1981
|
+
}
|
|
1982
|
+
function ingestStat(stat, out) {
|
|
1983
|
+
const id = String(stat["id"]);
|
|
1984
|
+
switch (stat["type"]) {
|
|
1985
|
+
case "local-candidate":
|
|
1986
|
+
out.localCands.set(id, stat);
|
|
1987
|
+
return;
|
|
1988
|
+
case "remote-candidate":
|
|
1989
|
+
out.remoteCands.set(id, stat);
|
|
1990
|
+
return;
|
|
1991
|
+
case "candidate-pair":
|
|
1992
|
+
out.pairs.set(id, stat);
|
|
1993
|
+
return;
|
|
1994
|
+
case "transport":
|
|
1995
|
+
ingestTransport(stat, out);
|
|
1996
|
+
return;
|
|
1997
|
+
case "data-channel":
|
|
1998
|
+
ingestDataChannel(stat, out);
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
function ingestTransport(stat, out) {
|
|
2003
|
+
const selectedId = stat["selectedCandidatePairId"];
|
|
2004
|
+
if (typeof selectedId === "string")
|
|
2005
|
+
out.selectedPairId = selectedId;
|
|
2006
|
+
const rp = stat["retransmittedPacketsSent"];
|
|
2007
|
+
const rb = stat["retransmittedBytesSent"];
|
|
2008
|
+
if (typeof rp === "number")
|
|
2009
|
+
out.retransmittedPacketsSent = rp;
|
|
2010
|
+
if (typeof rb === "number")
|
|
2011
|
+
out.retransmittedBytesSent = rb;
|
|
2012
|
+
}
|
|
2013
|
+
function ingestDataChannel(stat, out) {
|
|
2014
|
+
const rp = stat["retransmittedPacketsSent"];
|
|
2015
|
+
const rb = stat["retransmittedBytesSent"];
|
|
2016
|
+
if (out.retransmittedPacketsSent === undefined && typeof rp === "number") {
|
|
2017
|
+
out.retransmittedPacketsSent = rp;
|
|
2018
|
+
}
|
|
2019
|
+
if (out.retransmittedBytesSent === undefined && typeof rb === "number") {
|
|
2020
|
+
out.retransmittedBytesSent = rb;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
function selectActivePair(parsed) {
|
|
2024
|
+
if (parsed.selectedPairId) {
|
|
2025
|
+
const named = parsed.pairs.get(parsed.selectedPairId);
|
|
2026
|
+
if (named)
|
|
2027
|
+
return named;
|
|
2028
|
+
}
|
|
2029
|
+
for (const pair of parsed.pairs.values()) {
|
|
2030
|
+
if (pair["nominated"])
|
|
2031
|
+
return pair;
|
|
2032
|
+
}
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
function serialiseSlotView(slot) {
|
|
2036
|
+
return {
|
|
2037
|
+
signalingState: slot.connection.signalingState,
|
|
2038
|
+
iceConnectionState: slot.connection.iceConnectionState,
|
|
2039
|
+
connectionState: slot.connection.connectionState,
|
|
2040
|
+
dataChannelState: slot.channel?.readyState ?? "no-channel",
|
|
2041
|
+
pendingSendCount: slot.pendingSends.length,
|
|
2042
|
+
pendingRemoteIceCount: slot.pendingRemoteIce.length,
|
|
2043
|
+
inFlightSync: slot.inFlightSync ? { ...slot.inFlightSync } : undefined,
|
|
2044
|
+
transport: slot.transport ? {
|
|
2045
|
+
...slot.transport,
|
|
2046
|
+
selectedCandidatePair: slot.transport.selectedCandidatePair ? { ...slot.transport.selectedCandidatePair } : undefined
|
|
2047
|
+
} : undefined,
|
|
2048
|
+
lastSyncHandshakeAttempt: { ...slot.lastSyncHandshakeAttempt }
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
function emptySyncHandshakeAttempt() {
|
|
2052
|
+
return {
|
|
2053
|
+
dataChannelOpenedAt: undefined,
|
|
2054
|
+
peerCandidateEmittedAt: undefined,
|
|
2055
|
+
firstOutboundSendAt: undefined,
|
|
2056
|
+
firstInboundMessageAt: undefined
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
function buildCandidatePairView(pair, localCands, remoteCands) {
|
|
2060
|
+
const local = localCands.get(String(pair["localCandidateId"]));
|
|
2061
|
+
const remote = remoteCands.get(String(pair["remoteCandidateId"]));
|
|
2062
|
+
return {
|
|
2063
|
+
localCandidateType: String(local?.["candidateType"] ?? "?"),
|
|
2064
|
+
remoteCandidateType: String(remote?.["candidateType"] ?? "?"),
|
|
2065
|
+
state: String(pair["state"] ?? ""),
|
|
2066
|
+
nominated: Boolean(pair["nominated"]),
|
|
2067
|
+
bytesSent: Number(pair["bytesSent"] ?? 0),
|
|
2068
|
+
bytesReceived: Number(pair["bytesReceived"] ?? 0)
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
1936
2071
|
var DEFAULT_ICE_SERVERS = [
|
|
1937
2072
|
{ urls: "stun:stun.l.google.com:19302" },
|
|
1938
2073
|
{ urls: "stun:stun1.l.google.com:19302" }
|
|
@@ -1941,6 +2076,8 @@ var DEFAULT_ICE_SERVERS = [
|
|
|
1941
2076
|
class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
1942
2077
|
signaling;
|
|
1943
2078
|
iceServers;
|
|
2079
|
+
iceTransportPolicy;
|
|
2080
|
+
iceRelayEnforcement;
|
|
1944
2081
|
dataChannelLabel;
|
|
1945
2082
|
knownPeers;
|
|
1946
2083
|
keyringSource;
|
|
@@ -1954,6 +2091,9 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
1954
2091
|
slots = new Map;
|
|
1955
2092
|
ready = false;
|
|
1956
2093
|
readyResolver;
|
|
2094
|
+
lastSlotInitiationDecisions = new Map;
|
|
2095
|
+
sweepRunCount = 0;
|
|
2096
|
+
lastSweepAt;
|
|
1957
2097
|
get knownPeerIds() {
|
|
1958
2098
|
if (this.keyringSource !== undefined) {
|
|
1959
2099
|
return [...this.keyringSource().knownPeers.keys()].filter((id) => id !== this.localPeerId);
|
|
@@ -1971,6 +2111,8 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
1971
2111
|
super();
|
|
1972
2112
|
this.signaling = options.signaling;
|
|
1973
2113
|
this.iceServers = options.iceServers ?? DEFAULT_ICE_SERVERS;
|
|
2114
|
+
this.iceTransportPolicy = options.iceTransportPolicy;
|
|
2115
|
+
this.iceRelayEnforcement = options.iceRelayEnforcement ?? true;
|
|
1974
2116
|
this.dataChannelLabel = options.dataChannelLabel ?? "polly-mesh";
|
|
1975
2117
|
this.knownPeers = new Set(options.knownPeerIds ?? []);
|
|
1976
2118
|
this.keyringSource = options.keyringSource;
|
|
@@ -2004,25 +2146,26 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2004
2146
|
const peers = [];
|
|
2005
2147
|
for (const peerId of allPeers) {
|
|
2006
2148
|
const slot = this.slots.get(peerId);
|
|
2149
|
+
const decision = this.snapshotInitiationDecision(peerId);
|
|
2007
2150
|
peers.push({
|
|
2008
2151
|
peerId,
|
|
2009
2152
|
knownInKeyring: knownPeerSet.has(peerId),
|
|
2010
2153
|
presentInSignalling: this.presentPeers.has(peerId),
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
connectionState: slot.connection.connectionState,
|
|
2015
|
-
dataChannelState: slot.channel?.readyState ?? "no-channel",
|
|
2016
|
-
pendingSendCount: slot.pendingSends.length,
|
|
2017
|
-
pendingRemoteIceCount: slot.pendingRemoteIce.length,
|
|
2018
|
-
inFlightSync: slot.inFlightSync ? { ...slot.inFlightSync } : undefined
|
|
2019
|
-
} : undefined
|
|
2154
|
+
slotInitiationRejectionReason: decision.reason,
|
|
2155
|
+
slotInitiationDecision: decision,
|
|
2156
|
+
slot: slot ? serialiseSlotView(slot) : undefined
|
|
2020
2157
|
});
|
|
2021
2158
|
}
|
|
2022
2159
|
return {
|
|
2023
2160
|
localPeerId: this.localPeerId,
|
|
2024
2161
|
knownPeerIds,
|
|
2025
2162
|
presentPeerIds,
|
|
2163
|
+
sweep: {
|
|
2164
|
+
enabled: this.knownPeersRefreshTimer !== undefined,
|
|
2165
|
+
intervalMs: this.knownPeersRefreshIntervalMs,
|
|
2166
|
+
runCount: this.sweepRunCount,
|
|
2167
|
+
lastRunAt: this.lastSweepAt
|
|
2168
|
+
},
|
|
2026
2169
|
peers
|
|
2027
2170
|
};
|
|
2028
2171
|
}
|
|
@@ -2030,14 +2173,27 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2030
2173
|
this.presentPeers.add(remotePeerId);
|
|
2031
2174
|
if (!this.shouldInitiateTo(remotePeerId))
|
|
2032
2175
|
return;
|
|
2033
|
-
this.
|
|
2176
|
+
this.tryCreateInitiatingSlot(remotePeerId);
|
|
2034
2177
|
}
|
|
2035
2178
|
handlePeersPresent(peerIds) {
|
|
2036
2179
|
for (const remotePeerId of peerIds) {
|
|
2037
2180
|
this.presentPeers.add(remotePeerId);
|
|
2038
2181
|
if (!this.shouldInitiateTo(remotePeerId))
|
|
2039
2182
|
continue;
|
|
2183
|
+
this.tryCreateInitiatingSlot(remotePeerId);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
tryCreateInitiatingSlot(remotePeerId) {
|
|
2187
|
+
try {
|
|
2040
2188
|
this.createInitiatingSlot(remotePeerId);
|
|
2189
|
+
} catch (err) {
|
|
2190
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2191
|
+
this.lastSlotInitiationDecisions.set(remotePeerId, {
|
|
2192
|
+
decision: "rejected",
|
|
2193
|
+
reason: "fatal-error",
|
|
2194
|
+
error: message,
|
|
2195
|
+
at: performance.now()
|
|
2196
|
+
});
|
|
2041
2197
|
}
|
|
2042
2198
|
}
|
|
2043
2199
|
handlePeerLeft(remotePeerId) {
|
|
@@ -2061,25 +2217,49 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2061
2217
|
return;
|
|
2062
2218
|
if (!this.shouldInitiateTo(remotePeerId))
|
|
2063
2219
|
return;
|
|
2064
|
-
this.
|
|
2220
|
+
this.tryCreateInitiatingSlot(remotePeerId);
|
|
2065
2221
|
}
|
|
2066
2222
|
refreshKnownPeers() {
|
|
2067
2223
|
for (const remotePeerId of this.presentPeers) {
|
|
2068
2224
|
if (!this.shouldInitiateTo(remotePeerId))
|
|
2069
2225
|
continue;
|
|
2070
|
-
this.
|
|
2226
|
+
this.tryCreateInitiatingSlot(remotePeerId);
|
|
2071
2227
|
}
|
|
2072
2228
|
}
|
|
2073
2229
|
shouldInitiateTo(remotePeerId) {
|
|
2230
|
+
const reason = this.evaluateInitiation(remotePeerId);
|
|
2231
|
+
this.lastSlotInitiationDecisions.set(remotePeerId, {
|
|
2232
|
+
decision: reason === undefined ? "accepted" : "rejected",
|
|
2233
|
+
reason,
|
|
2234
|
+
error: undefined,
|
|
2235
|
+
at: performance.now()
|
|
2236
|
+
});
|
|
2237
|
+
return reason === undefined;
|
|
2238
|
+
}
|
|
2239
|
+
evaluateInitiation(remotePeerId) {
|
|
2074
2240
|
if (remotePeerId === this.localPeerId)
|
|
2075
|
-
return
|
|
2241
|
+
return "self";
|
|
2076
2242
|
if (!this.hasKnownPeer(remotePeerId))
|
|
2077
|
-
return
|
|
2243
|
+
return "not-in-keyring";
|
|
2244
|
+
if (!this.presentPeers.has(remotePeerId))
|
|
2245
|
+
return "not-present";
|
|
2078
2246
|
if (this.slots.has(remotePeerId))
|
|
2079
|
-
return
|
|
2247
|
+
return "slot-already-exists";
|
|
2080
2248
|
if (this.localPeerId <= remotePeerId)
|
|
2081
|
-
return
|
|
2082
|
-
return
|
|
2249
|
+
return "tie-break-other-side";
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
snapshotInitiationDecision(remotePeerId) {
|
|
2253
|
+
const cached = this.lastSlotInitiationDecisions.get(remotePeerId);
|
|
2254
|
+
if (cached?.reason === "fatal-error")
|
|
2255
|
+
return cached;
|
|
2256
|
+
const reason = this.evaluateInitiation(remotePeerId);
|
|
2257
|
+
return {
|
|
2258
|
+
decision: reason === undefined ? "accepted" : "rejected",
|
|
2259
|
+
reason,
|
|
2260
|
+
error: undefined,
|
|
2261
|
+
at: performance.now()
|
|
2262
|
+
};
|
|
2083
2263
|
}
|
|
2084
2264
|
whenReady() {
|
|
2085
2265
|
if (this.ready)
|
|
@@ -2116,7 +2296,11 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2116
2296
|
if (this.knownPeersRefreshTimer !== undefined)
|
|
2117
2297
|
return;
|
|
2118
2298
|
this.knownPeersRefreshTimer = setInterval(() => {
|
|
2119
|
-
this.
|
|
2299
|
+
this.sweepRunCount += 1;
|
|
2300
|
+
this.lastSweepAt = performance.now();
|
|
2301
|
+
try {
|
|
2302
|
+
this.refreshKnownPeers();
|
|
2303
|
+
} catch {}
|
|
2120
2304
|
}, this.knownPeersRefreshIntervalMs);
|
|
2121
2305
|
}
|
|
2122
2306
|
stopKnownPeersSweep() {
|
|
@@ -2132,6 +2316,9 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2132
2316
|
if (!slot) {
|
|
2133
2317
|
slot = this.createInitiatingSlot(targetId);
|
|
2134
2318
|
}
|
|
2319
|
+
if (slot.lastSyncHandshakeAttempt.firstOutboundSendAt === undefined) {
|
|
2320
|
+
slot.lastSyncHandshakeAttempt.firstOutboundSendAt = performance.now();
|
|
2321
|
+
}
|
|
2135
2322
|
if (slot.channel && slot.channel.readyState === "open") {
|
|
2136
2323
|
this.sendBytesMaybeFragmented(slot.channel, bytes);
|
|
2137
2324
|
} else {
|
|
@@ -2180,8 +2367,61 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2180
2367
|
return;
|
|
2181
2368
|
}
|
|
2182
2369
|
}
|
|
2370
|
+
buildRtcConfiguration() {
|
|
2371
|
+
const config = { iceServers: this.iceServers };
|
|
2372
|
+
if (this.iceTransportPolicy !== undefined) {
|
|
2373
|
+
config.iceTransportPolicy = this.iceTransportPolicy;
|
|
2374
|
+
}
|
|
2375
|
+
return config;
|
|
2376
|
+
}
|
|
2377
|
+
isRelayCandidateInit(init) {
|
|
2378
|
+
const candidateStr = init.candidate ?? "";
|
|
2379
|
+
const m = candidateStr.match(/\btyp\s+(\S+)/i);
|
|
2380
|
+
return m?.[1]?.toLowerCase() === "relay";
|
|
2381
|
+
}
|
|
2382
|
+
shouldSendCandidate(candidate) {
|
|
2383
|
+
if (!this.iceRelayEnforcement)
|
|
2384
|
+
return true;
|
|
2385
|
+
if (this.iceTransportPolicy !== "relay")
|
|
2386
|
+
return true;
|
|
2387
|
+
const typed = candidate.type;
|
|
2388
|
+
const match = candidate.candidate.match(/\btyp\s+(\S+)/i);
|
|
2389
|
+
const candidateType = (typed ?? match?.[1] ?? "").toLowerCase();
|
|
2390
|
+
return candidateType === "relay";
|
|
2391
|
+
}
|
|
2392
|
+
filterSdpCandidatesByPolicy(sdp) {
|
|
2393
|
+
if (!this.iceRelayEnforcement)
|
|
2394
|
+
return sdp;
|
|
2395
|
+
if (this.iceTransportPolicy !== "relay")
|
|
2396
|
+
return sdp;
|
|
2397
|
+
if (!sdp)
|
|
2398
|
+
return sdp;
|
|
2399
|
+
const lines = sdp.split(/\r?\n/);
|
|
2400
|
+
const filtered = [];
|
|
2401
|
+
for (const line of lines) {
|
|
2402
|
+
if (!line.startsWith("a=candidate:")) {
|
|
2403
|
+
filtered.push(line);
|
|
2404
|
+
continue;
|
|
2405
|
+
}
|
|
2406
|
+
const m = line.match(/\btyp\s+(\S+)/i);
|
|
2407
|
+
if (m?.[1]?.toLowerCase() === "relay")
|
|
2408
|
+
filtered.push(line);
|
|
2409
|
+
}
|
|
2410
|
+
return filtered.join(`\r
|
|
2411
|
+
`);
|
|
2412
|
+
}
|
|
2413
|
+
applySdpPolicyFilter(description) {
|
|
2414
|
+
if (!this.iceRelayEnforcement)
|
|
2415
|
+
return description;
|
|
2416
|
+
if (this.iceTransportPolicy !== "relay")
|
|
2417
|
+
return description;
|
|
2418
|
+
const filtered = this.filterSdpCandidatesByPolicy(description.sdp);
|
|
2419
|
+
if (filtered === description.sdp)
|
|
2420
|
+
return description;
|
|
2421
|
+
return { ...description, sdp: filtered };
|
|
2422
|
+
}
|
|
2183
2423
|
createInitiatingSlot(targetId) {
|
|
2184
|
-
const connection = new this.RTCPeerConnectionCtor(
|
|
2424
|
+
const connection = new this.RTCPeerConnectionCtor(this.buildRtcConfiguration());
|
|
2185
2425
|
const channel = connection.createDataChannel(this.dataChannelLabel, { ordered: true });
|
|
2186
2426
|
const slot = {
|
|
2187
2427
|
connection,
|
|
@@ -2189,7 +2429,10 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2189
2429
|
pendingSends: [],
|
|
2190
2430
|
pendingFragments: new Map,
|
|
2191
2431
|
pendingRemoteIce: [],
|
|
2192
|
-
inFlightSync: undefined
|
|
2432
|
+
inFlightSync: undefined,
|
|
2433
|
+
transport: undefined,
|
|
2434
|
+
lastDataChannelError: undefined,
|
|
2435
|
+
lastSyncHandshakeAttempt: emptySyncHandshakeAttempt()
|
|
2193
2436
|
};
|
|
2194
2437
|
this.slots.set(targetId, slot);
|
|
2195
2438
|
this.wireConnection(targetId, connection);
|
|
@@ -2200,7 +2443,15 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2200
2443
|
async initiateOffer(targetId, connection) {
|
|
2201
2444
|
const offer = await connection.createOffer();
|
|
2202
2445
|
await connection.setLocalDescription(offer);
|
|
2203
|
-
|
|
2446
|
+
const localOffer = connection.localDescription ?? offer;
|
|
2447
|
+
const sdpToSend = this.applySdpPolicyFilter({
|
|
2448
|
+
type: localOffer.type,
|
|
2449
|
+
sdp: localOffer.sdp ?? offer.sdp
|
|
2450
|
+
});
|
|
2451
|
+
this.signaling.sendSignal(targetId, {
|
|
2452
|
+
kind: "offer",
|
|
2453
|
+
sdp: sdpToSend
|
|
2454
|
+
});
|
|
2204
2455
|
}
|
|
2205
2456
|
async handleOffer(fromPeerId, sdp) {
|
|
2206
2457
|
const existing = this.slots.get(fromPeerId);
|
|
@@ -2213,14 +2464,17 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2213
2464
|
existing.connection.close();
|
|
2214
2465
|
this.slots.delete(fromPeerId);
|
|
2215
2466
|
}
|
|
2216
|
-
const connection = new this.RTCPeerConnectionCtor(
|
|
2467
|
+
const connection = new this.RTCPeerConnectionCtor(this.buildRtcConfiguration());
|
|
2217
2468
|
const slot = {
|
|
2218
2469
|
connection,
|
|
2219
2470
|
channel: undefined,
|
|
2220
2471
|
pendingSends: [],
|
|
2221
2472
|
pendingFragments: new Map,
|
|
2222
2473
|
pendingRemoteIce: [],
|
|
2223
|
-
inFlightSync: undefined
|
|
2474
|
+
inFlightSync: undefined,
|
|
2475
|
+
transport: undefined,
|
|
2476
|
+
lastDataChannelError: undefined,
|
|
2477
|
+
lastSyncHandshakeAttempt: emptySyncHandshakeAttempt()
|
|
2224
2478
|
};
|
|
2225
2479
|
this.slots.set(fromPeerId, slot);
|
|
2226
2480
|
this.wireConnection(fromPeerId, connection);
|
|
@@ -2228,13 +2482,18 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2228
2482
|
slot.channel = event.channel;
|
|
2229
2483
|
this.wireDataChannel(fromPeerId, event.channel);
|
|
2230
2484
|
};
|
|
2231
|
-
await connection.setRemoteDescription(sdp);
|
|
2485
|
+
await connection.setRemoteDescription(this.applySdpPolicyFilter(sdp));
|
|
2232
2486
|
await this.flushPendingRemoteIce(slot);
|
|
2233
2487
|
const answer = await connection.createAnswer();
|
|
2234
2488
|
await connection.setLocalDescription(answer);
|
|
2489
|
+
const localAnswer = connection.localDescription ?? answer;
|
|
2490
|
+
const sdpToSend = this.applySdpPolicyFilter({
|
|
2491
|
+
type: localAnswer.type,
|
|
2492
|
+
sdp: localAnswer.sdp ?? answer.sdp
|
|
2493
|
+
});
|
|
2235
2494
|
this.signaling.sendSignal(fromPeerId, {
|
|
2236
2495
|
kind: "answer",
|
|
2237
|
-
sdp:
|
|
2496
|
+
sdp: sdpToSend
|
|
2238
2497
|
});
|
|
2239
2498
|
}
|
|
2240
2499
|
async handleAnswer(fromPeerId, sdp) {
|
|
@@ -2243,13 +2502,15 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2243
2502
|
return;
|
|
2244
2503
|
if (slot.connection.signalingState !== "have-local-offer")
|
|
2245
2504
|
return;
|
|
2246
|
-
await slot.connection.setRemoteDescription(sdp);
|
|
2505
|
+
await slot.connection.setRemoteDescription(this.applySdpPolicyFilter(sdp));
|
|
2247
2506
|
await this.flushPendingRemoteIce(slot);
|
|
2248
2507
|
}
|
|
2249
2508
|
async handleIceCandidate(fromPeerId, candidate) {
|
|
2250
2509
|
const slot = this.slots.get(fromPeerId);
|
|
2251
2510
|
if (!slot)
|
|
2252
2511
|
return;
|
|
2512
|
+
if (this.iceRelayEnforcement && this.iceTransportPolicy === "relay" && !this.isRelayCandidateInit(candidate))
|
|
2513
|
+
return;
|
|
2253
2514
|
if (slot.connection.remoteDescription === null) {
|
|
2254
2515
|
slot.pendingRemoteIce.push(candidate);
|
|
2255
2516
|
return;
|
|
@@ -2271,31 +2532,44 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2271
2532
|
}
|
|
2272
2533
|
wireConnection(peerId, connection) {
|
|
2273
2534
|
connection.onicecandidate = (event) => {
|
|
2274
|
-
if (event.candidate)
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2535
|
+
if (!event.candidate)
|
|
2536
|
+
return;
|
|
2537
|
+
if (!this.shouldSendCandidate(event.candidate))
|
|
2538
|
+
return;
|
|
2539
|
+
this.signaling.sendSignal(peerId, {
|
|
2540
|
+
kind: "ice",
|
|
2541
|
+
candidate: event.candidate.toJSON()
|
|
2542
|
+
});
|
|
2280
2543
|
};
|
|
2281
2544
|
connection.onconnectionstatechange = () => {
|
|
2282
2545
|
const state = connection.connectionState;
|
|
2283
2546
|
if (state === "connected") {
|
|
2284
|
-
this.
|
|
2285
|
-
peerId,
|
|
2286
|
-
peerMetadata: {}
|
|
2287
|
-
});
|
|
2547
|
+
this.emitPeerCandidateOnce(peerId);
|
|
2288
2548
|
} else if (state === "disconnected" || state === "failed" || state === "closed") {
|
|
2289
2549
|
this.slots.delete(peerId);
|
|
2290
2550
|
this.emit("peer-disconnected", { peerId });
|
|
2291
2551
|
}
|
|
2292
2552
|
};
|
|
2293
2553
|
}
|
|
2554
|
+
emitPeerCandidateOnce(peerId) {
|
|
2555
|
+
const slot = this.slots.get(peerId);
|
|
2556
|
+
if (!slot)
|
|
2557
|
+
return;
|
|
2558
|
+
if (slot.lastSyncHandshakeAttempt.peerCandidateEmittedAt !== undefined)
|
|
2559
|
+
return;
|
|
2560
|
+
slot.lastSyncHandshakeAttempt.peerCandidateEmittedAt = performance.now();
|
|
2561
|
+
this.emit("peer-candidate", {
|
|
2562
|
+
peerId,
|
|
2563
|
+
peerMetadata: {}
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2294
2566
|
wireDataChannel(peerId, channel) {
|
|
2295
2567
|
channel.onopen = () => {
|
|
2296
2568
|
const slot = this.slots.get(peerId);
|
|
2297
2569
|
if (!slot)
|
|
2298
2570
|
return;
|
|
2571
|
+
slot.lastSyncHandshakeAttempt.dataChannelOpenedAt = performance.now();
|
|
2572
|
+
this.emitPeerCandidateOnce(peerId);
|
|
2299
2573
|
for (const bytes of slot.pendingSends) {
|
|
2300
2574
|
this.sendBytesMaybeFragmented(channel, bytes);
|
|
2301
2575
|
}
|
|
@@ -2315,6 +2589,34 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2315
2589
|
slot.channel = undefined;
|
|
2316
2590
|
}
|
|
2317
2591
|
};
|
|
2592
|
+
channel.onerror = (event) => {
|
|
2593
|
+
const slot = this.slots.get(peerId);
|
|
2594
|
+
if (!slot)
|
|
2595
|
+
return;
|
|
2596
|
+
const ev = event;
|
|
2597
|
+
const message = typeof ev.error?.message === "string" ? ev.error.message : typeof ev.message === "string" ? ev.message : "unknown data channel error";
|
|
2598
|
+
slot.lastDataChannelError = message;
|
|
2599
|
+
};
|
|
2600
|
+
}
|
|
2601
|
+
async refreshTransportStats(peerId) {
|
|
2602
|
+
const slot = this.slots.get(peerId);
|
|
2603
|
+
if (!slot)
|
|
2604
|
+
return;
|
|
2605
|
+
const snapshot = await collectTransportSnapshot(slot.connection, slot.lastDataChannelError);
|
|
2606
|
+
slot.transport = snapshot;
|
|
2607
|
+
return snapshot;
|
|
2608
|
+
}
|
|
2609
|
+
async refreshAllTransportStats() {
|
|
2610
|
+
const out = new Map;
|
|
2611
|
+
const peerIds = [...this.slots.keys()];
|
|
2612
|
+
const results = await Promise.all(peerIds.map((id) => this.refreshTransportStats(id)));
|
|
2613
|
+
for (let i = 0;i < peerIds.length; i += 1) {
|
|
2614
|
+
const r = results[i];
|
|
2615
|
+
const id = peerIds[i];
|
|
2616
|
+
if (r && id !== undefined)
|
|
2617
|
+
out.set(id, r);
|
|
2618
|
+
}
|
|
2619
|
+
return out;
|
|
2318
2620
|
}
|
|
2319
2621
|
dispatchMessage(fromPeerId, bytes) {
|
|
2320
2622
|
try {
|
|
@@ -2335,6 +2637,7 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2335
2637
|
} catch {}
|
|
2336
2638
|
}
|
|
2337
2639
|
scheduleEmitMessage(fromPeerId, message, viaFragmentPath) {
|
|
2640
|
+
this.stampFirstInboundMessage(fromPeerId);
|
|
2338
2641
|
if (!this.syncYieldEnabled) {
|
|
2339
2642
|
this.emit("message", message);
|
|
2340
2643
|
if (viaFragmentPath) {
|
|
@@ -2358,6 +2661,14 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2358
2661
|
}
|
|
2359
2662
|
}, 0);
|
|
2360
2663
|
}
|
|
2664
|
+
stampFirstInboundMessage(fromPeerId) {
|
|
2665
|
+
const slot = this.slots.get(fromPeerId);
|
|
2666
|
+
if (!slot)
|
|
2667
|
+
return;
|
|
2668
|
+
if (slot.lastSyncHandshakeAttempt.firstInboundMessageAt !== undefined)
|
|
2669
|
+
return;
|
|
2670
|
+
slot.lastSyncHandshakeAttempt.firstInboundMessageAt = performance.now();
|
|
2671
|
+
}
|
|
2361
2672
|
finishInFlightSyncApply(fromPeerId) {
|
|
2362
2673
|
const slot = this.slots.get(fromPeerId);
|
|
2363
2674
|
if (!slot?.inFlightSync)
|
|
@@ -2514,6 +2825,12 @@ async function createMeshClient(options) {
|
|
|
2514
2825
|
knownPeerIds,
|
|
2515
2826
|
keyringSource,
|
|
2516
2827
|
...resolvedIceServers !== undefined && { iceServers: resolvedIceServers },
|
|
2828
|
+
...options.rtc?.iceTransportPolicy !== undefined && {
|
|
2829
|
+
iceTransportPolicy: options.rtc.iceTransportPolicy
|
|
2830
|
+
},
|
|
2831
|
+
...options.rtc?.iceRelayEnforcement !== undefined && {
|
|
2832
|
+
iceRelayEnforcement: options.rtc.iceRelayEnforcement
|
|
2833
|
+
},
|
|
2517
2834
|
...options.rtc?.dataChannelLabel !== undefined && {
|
|
2518
2835
|
dataChannelLabel: options.rtc.dataChannelLabel
|
|
2519
2836
|
},
|
|
@@ -2583,11 +2900,22 @@ async function createMeshClient(options) {
|
|
|
2583
2900
|
localPeerId: options.signaling.peerId,
|
|
2584
2901
|
knownPeerIds: [],
|
|
2585
2902
|
presentPeerIds: [],
|
|
2903
|
+
sweep: {
|
|
2904
|
+
enabled: false,
|
|
2905
|
+
intervalMs: 0,
|
|
2906
|
+
runCount: 0,
|
|
2907
|
+
lastRunAt: undefined
|
|
2908
|
+
},
|
|
2586
2909
|
peers: []
|
|
2587
2910
|
};
|
|
2588
2911
|
}
|
|
2589
2912
|
return webrtcAdapter.getPeerStateSnapshot();
|
|
2590
2913
|
},
|
|
2914
|
+
refreshTransportStats: async () => {
|
|
2915
|
+
if (!webrtcAdapter)
|
|
2916
|
+
return;
|
|
2917
|
+
await webrtcAdapter.refreshAllTransportStats();
|
|
2918
|
+
},
|
|
2591
2919
|
close: async () => {
|
|
2592
2920
|
signaling.close();
|
|
2593
2921
|
webrtcAdapter?.disconnect();
|
|
@@ -3002,4 +3330,4 @@ export {
|
|
|
3002
3330
|
$meshCounter
|
|
3003
3331
|
};
|
|
3004
3332
|
|
|
3005
|
-
//# debugId=
|
|
3333
|
+
//# debugId=27D9577FC215E14564756E2164756E21
|