@fairfox/polly 0.54.0 → 0.55.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.js
CHANGED
|
@@ -1933,6 +1933,132 @@ 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
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
function buildCandidatePairView(pair, localCands, remoteCands) {
|
|
2051
|
+
const local = localCands.get(String(pair["localCandidateId"]));
|
|
2052
|
+
const remote = remoteCands.get(String(pair["remoteCandidateId"]));
|
|
2053
|
+
return {
|
|
2054
|
+
localCandidateType: String(local?.["candidateType"] ?? "?"),
|
|
2055
|
+
remoteCandidateType: String(remote?.["candidateType"] ?? "?"),
|
|
2056
|
+
state: String(pair["state"] ?? ""),
|
|
2057
|
+
nominated: Boolean(pair["nominated"]),
|
|
2058
|
+
bytesSent: Number(pair["bytesSent"] ?? 0),
|
|
2059
|
+
bytesReceived: Number(pair["bytesReceived"] ?? 0)
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
1936
2062
|
var DEFAULT_ICE_SERVERS = [
|
|
1937
2063
|
{ urls: "stun:stun.l.google.com:19302" },
|
|
1938
2064
|
{ urls: "stun:stun1.l.google.com:19302" }
|
|
@@ -1941,6 +2067,8 @@ var DEFAULT_ICE_SERVERS = [
|
|
|
1941
2067
|
class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
1942
2068
|
signaling;
|
|
1943
2069
|
iceServers;
|
|
2070
|
+
iceTransportPolicy;
|
|
2071
|
+
iceRelayEnforcement;
|
|
1944
2072
|
dataChannelLabel;
|
|
1945
2073
|
knownPeers;
|
|
1946
2074
|
keyringSource;
|
|
@@ -1971,6 +2099,8 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
1971
2099
|
super();
|
|
1972
2100
|
this.signaling = options.signaling;
|
|
1973
2101
|
this.iceServers = options.iceServers ?? DEFAULT_ICE_SERVERS;
|
|
2102
|
+
this.iceTransportPolicy = options.iceTransportPolicy;
|
|
2103
|
+
this.iceRelayEnforcement = options.iceRelayEnforcement ?? true;
|
|
1974
2104
|
this.dataChannelLabel = options.dataChannelLabel ?? "polly-mesh";
|
|
1975
2105
|
this.knownPeers = new Set(options.knownPeerIds ?? []);
|
|
1976
2106
|
this.keyringSource = options.keyringSource;
|
|
@@ -2008,15 +2138,7 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2008
2138
|
peerId,
|
|
2009
2139
|
knownInKeyring: knownPeerSet.has(peerId),
|
|
2010
2140
|
presentInSignalling: this.presentPeers.has(peerId),
|
|
2011
|
-
slot: slot ?
|
|
2012
|
-
signalingState: slot.connection.signalingState,
|
|
2013
|
-
iceConnectionState: slot.connection.iceConnectionState,
|
|
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
|
|
2141
|
+
slot: slot ? serialiseSlotView(slot) : undefined
|
|
2020
2142
|
});
|
|
2021
2143
|
}
|
|
2022
2144
|
return {
|
|
@@ -2180,8 +2302,61 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2180
2302
|
return;
|
|
2181
2303
|
}
|
|
2182
2304
|
}
|
|
2305
|
+
buildRtcConfiguration() {
|
|
2306
|
+
const config = { iceServers: this.iceServers };
|
|
2307
|
+
if (this.iceTransportPolicy !== undefined) {
|
|
2308
|
+
config.iceTransportPolicy = this.iceTransportPolicy;
|
|
2309
|
+
}
|
|
2310
|
+
return config;
|
|
2311
|
+
}
|
|
2312
|
+
isRelayCandidateInit(init) {
|
|
2313
|
+
const candidateStr = init.candidate ?? "";
|
|
2314
|
+
const m = candidateStr.match(/\btyp\s+(\S+)/i);
|
|
2315
|
+
return m?.[1]?.toLowerCase() === "relay";
|
|
2316
|
+
}
|
|
2317
|
+
shouldSendCandidate(candidate) {
|
|
2318
|
+
if (!this.iceRelayEnforcement)
|
|
2319
|
+
return true;
|
|
2320
|
+
if (this.iceTransportPolicy !== "relay")
|
|
2321
|
+
return true;
|
|
2322
|
+
const typed = candidate.type;
|
|
2323
|
+
const match = candidate.candidate.match(/\btyp\s+(\S+)/i);
|
|
2324
|
+
const candidateType = (typed ?? match?.[1] ?? "").toLowerCase();
|
|
2325
|
+
return candidateType === "relay";
|
|
2326
|
+
}
|
|
2327
|
+
filterSdpCandidatesByPolicy(sdp) {
|
|
2328
|
+
if (!this.iceRelayEnforcement)
|
|
2329
|
+
return sdp;
|
|
2330
|
+
if (this.iceTransportPolicy !== "relay")
|
|
2331
|
+
return sdp;
|
|
2332
|
+
if (!sdp)
|
|
2333
|
+
return sdp;
|
|
2334
|
+
const lines = sdp.split(/\r?\n/);
|
|
2335
|
+
const filtered = [];
|
|
2336
|
+
for (const line of lines) {
|
|
2337
|
+
if (!line.startsWith("a=candidate:")) {
|
|
2338
|
+
filtered.push(line);
|
|
2339
|
+
continue;
|
|
2340
|
+
}
|
|
2341
|
+
const m = line.match(/\btyp\s+(\S+)/i);
|
|
2342
|
+
if (m?.[1]?.toLowerCase() === "relay")
|
|
2343
|
+
filtered.push(line);
|
|
2344
|
+
}
|
|
2345
|
+
return filtered.join(`\r
|
|
2346
|
+
`);
|
|
2347
|
+
}
|
|
2348
|
+
applySdpPolicyFilter(description) {
|
|
2349
|
+
if (!this.iceRelayEnforcement)
|
|
2350
|
+
return description;
|
|
2351
|
+
if (this.iceTransportPolicy !== "relay")
|
|
2352
|
+
return description;
|
|
2353
|
+
const filtered = this.filterSdpCandidatesByPolicy(description.sdp);
|
|
2354
|
+
if (filtered === description.sdp)
|
|
2355
|
+
return description;
|
|
2356
|
+
return { ...description, sdp: filtered };
|
|
2357
|
+
}
|
|
2183
2358
|
createInitiatingSlot(targetId) {
|
|
2184
|
-
const connection = new this.RTCPeerConnectionCtor(
|
|
2359
|
+
const connection = new this.RTCPeerConnectionCtor(this.buildRtcConfiguration());
|
|
2185
2360
|
const channel = connection.createDataChannel(this.dataChannelLabel, { ordered: true });
|
|
2186
2361
|
const slot = {
|
|
2187
2362
|
connection,
|
|
@@ -2189,7 +2364,9 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2189
2364
|
pendingSends: [],
|
|
2190
2365
|
pendingFragments: new Map,
|
|
2191
2366
|
pendingRemoteIce: [],
|
|
2192
|
-
inFlightSync: undefined
|
|
2367
|
+
inFlightSync: undefined,
|
|
2368
|
+
transport: undefined,
|
|
2369
|
+
lastDataChannelError: undefined
|
|
2193
2370
|
};
|
|
2194
2371
|
this.slots.set(targetId, slot);
|
|
2195
2372
|
this.wireConnection(targetId, connection);
|
|
@@ -2200,7 +2377,15 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2200
2377
|
async initiateOffer(targetId, connection) {
|
|
2201
2378
|
const offer = await connection.createOffer();
|
|
2202
2379
|
await connection.setLocalDescription(offer);
|
|
2203
|
-
|
|
2380
|
+
const localOffer = connection.localDescription ?? offer;
|
|
2381
|
+
const sdpToSend = this.applySdpPolicyFilter({
|
|
2382
|
+
type: localOffer.type,
|
|
2383
|
+
sdp: localOffer.sdp ?? offer.sdp
|
|
2384
|
+
});
|
|
2385
|
+
this.signaling.sendSignal(targetId, {
|
|
2386
|
+
kind: "offer",
|
|
2387
|
+
sdp: sdpToSend
|
|
2388
|
+
});
|
|
2204
2389
|
}
|
|
2205
2390
|
async handleOffer(fromPeerId, sdp) {
|
|
2206
2391
|
const existing = this.slots.get(fromPeerId);
|
|
@@ -2213,14 +2398,16 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2213
2398
|
existing.connection.close();
|
|
2214
2399
|
this.slots.delete(fromPeerId);
|
|
2215
2400
|
}
|
|
2216
|
-
const connection = new this.RTCPeerConnectionCtor(
|
|
2401
|
+
const connection = new this.RTCPeerConnectionCtor(this.buildRtcConfiguration());
|
|
2217
2402
|
const slot = {
|
|
2218
2403
|
connection,
|
|
2219
2404
|
channel: undefined,
|
|
2220
2405
|
pendingSends: [],
|
|
2221
2406
|
pendingFragments: new Map,
|
|
2222
2407
|
pendingRemoteIce: [],
|
|
2223
|
-
inFlightSync: undefined
|
|
2408
|
+
inFlightSync: undefined,
|
|
2409
|
+
transport: undefined,
|
|
2410
|
+
lastDataChannelError: undefined
|
|
2224
2411
|
};
|
|
2225
2412
|
this.slots.set(fromPeerId, slot);
|
|
2226
2413
|
this.wireConnection(fromPeerId, connection);
|
|
@@ -2228,13 +2415,18 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2228
2415
|
slot.channel = event.channel;
|
|
2229
2416
|
this.wireDataChannel(fromPeerId, event.channel);
|
|
2230
2417
|
};
|
|
2231
|
-
await connection.setRemoteDescription(sdp);
|
|
2418
|
+
await connection.setRemoteDescription(this.applySdpPolicyFilter(sdp));
|
|
2232
2419
|
await this.flushPendingRemoteIce(slot);
|
|
2233
2420
|
const answer = await connection.createAnswer();
|
|
2234
2421
|
await connection.setLocalDescription(answer);
|
|
2422
|
+
const localAnswer = connection.localDescription ?? answer;
|
|
2423
|
+
const sdpToSend = this.applySdpPolicyFilter({
|
|
2424
|
+
type: localAnswer.type,
|
|
2425
|
+
sdp: localAnswer.sdp ?? answer.sdp
|
|
2426
|
+
});
|
|
2235
2427
|
this.signaling.sendSignal(fromPeerId, {
|
|
2236
2428
|
kind: "answer",
|
|
2237
|
-
sdp:
|
|
2429
|
+
sdp: sdpToSend
|
|
2238
2430
|
});
|
|
2239
2431
|
}
|
|
2240
2432
|
async handleAnswer(fromPeerId, sdp) {
|
|
@@ -2243,13 +2435,15 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2243
2435
|
return;
|
|
2244
2436
|
if (slot.connection.signalingState !== "have-local-offer")
|
|
2245
2437
|
return;
|
|
2246
|
-
await slot.connection.setRemoteDescription(sdp);
|
|
2438
|
+
await slot.connection.setRemoteDescription(this.applySdpPolicyFilter(sdp));
|
|
2247
2439
|
await this.flushPendingRemoteIce(slot);
|
|
2248
2440
|
}
|
|
2249
2441
|
async handleIceCandidate(fromPeerId, candidate) {
|
|
2250
2442
|
const slot = this.slots.get(fromPeerId);
|
|
2251
2443
|
if (!slot)
|
|
2252
2444
|
return;
|
|
2445
|
+
if (this.iceRelayEnforcement && this.iceTransportPolicy === "relay" && !this.isRelayCandidateInit(candidate))
|
|
2446
|
+
return;
|
|
2253
2447
|
if (slot.connection.remoteDescription === null) {
|
|
2254
2448
|
slot.pendingRemoteIce.push(candidate);
|
|
2255
2449
|
return;
|
|
@@ -2271,12 +2465,14 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2271
2465
|
}
|
|
2272
2466
|
wireConnection(peerId, connection) {
|
|
2273
2467
|
connection.onicecandidate = (event) => {
|
|
2274
|
-
if (event.candidate)
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2468
|
+
if (!event.candidate)
|
|
2469
|
+
return;
|
|
2470
|
+
if (!this.shouldSendCandidate(event.candidate))
|
|
2471
|
+
return;
|
|
2472
|
+
this.signaling.sendSignal(peerId, {
|
|
2473
|
+
kind: "ice",
|
|
2474
|
+
candidate: event.candidate.toJSON()
|
|
2475
|
+
});
|
|
2280
2476
|
};
|
|
2281
2477
|
connection.onconnectionstatechange = () => {
|
|
2282
2478
|
const state = connection.connectionState;
|
|
@@ -2315,6 +2511,34 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2315
2511
|
slot.channel = undefined;
|
|
2316
2512
|
}
|
|
2317
2513
|
};
|
|
2514
|
+
channel.onerror = (event) => {
|
|
2515
|
+
const slot = this.slots.get(peerId);
|
|
2516
|
+
if (!slot)
|
|
2517
|
+
return;
|
|
2518
|
+
const ev = event;
|
|
2519
|
+
const message = typeof ev.error?.message === "string" ? ev.error.message : typeof ev.message === "string" ? ev.message : "unknown data channel error";
|
|
2520
|
+
slot.lastDataChannelError = message;
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
2523
|
+
async refreshTransportStats(peerId) {
|
|
2524
|
+
const slot = this.slots.get(peerId);
|
|
2525
|
+
if (!slot)
|
|
2526
|
+
return;
|
|
2527
|
+
const snapshot = await collectTransportSnapshot(slot.connection, slot.lastDataChannelError);
|
|
2528
|
+
slot.transport = snapshot;
|
|
2529
|
+
return snapshot;
|
|
2530
|
+
}
|
|
2531
|
+
async refreshAllTransportStats() {
|
|
2532
|
+
const out = new Map;
|
|
2533
|
+
const peerIds = [...this.slots.keys()];
|
|
2534
|
+
const results = await Promise.all(peerIds.map((id) => this.refreshTransportStats(id)));
|
|
2535
|
+
for (let i = 0;i < peerIds.length; i += 1) {
|
|
2536
|
+
const r = results[i];
|
|
2537
|
+
const id = peerIds[i];
|
|
2538
|
+
if (r && id !== undefined)
|
|
2539
|
+
out.set(id, r);
|
|
2540
|
+
}
|
|
2541
|
+
return out;
|
|
2318
2542
|
}
|
|
2319
2543
|
dispatchMessage(fromPeerId, bytes) {
|
|
2320
2544
|
try {
|
|
@@ -2514,6 +2738,12 @@ async function createMeshClient(options) {
|
|
|
2514
2738
|
knownPeerIds,
|
|
2515
2739
|
keyringSource,
|
|
2516
2740
|
...resolvedIceServers !== undefined && { iceServers: resolvedIceServers },
|
|
2741
|
+
...options.rtc?.iceTransportPolicy !== undefined && {
|
|
2742
|
+
iceTransportPolicy: options.rtc.iceTransportPolicy
|
|
2743
|
+
},
|
|
2744
|
+
...options.rtc?.iceRelayEnforcement !== undefined && {
|
|
2745
|
+
iceRelayEnforcement: options.rtc.iceRelayEnforcement
|
|
2746
|
+
},
|
|
2517
2747
|
...options.rtc?.dataChannelLabel !== undefined && {
|
|
2518
2748
|
dataChannelLabel: options.rtc.dataChannelLabel
|
|
2519
2749
|
},
|
|
@@ -2588,6 +2818,11 @@ async function createMeshClient(options) {
|
|
|
2588
2818
|
}
|
|
2589
2819
|
return webrtcAdapter.getPeerStateSnapshot();
|
|
2590
2820
|
},
|
|
2821
|
+
refreshTransportStats: async () => {
|
|
2822
|
+
if (!webrtcAdapter)
|
|
2823
|
+
return;
|
|
2824
|
+
await webrtcAdapter.refreshAllTransportStats();
|
|
2825
|
+
},
|
|
2591
2826
|
close: async () => {
|
|
2592
2827
|
signaling.close();
|
|
2593
2828
|
webrtcAdapter?.disconnect();
|
|
@@ -3002,4 +3237,4 @@ export {
|
|
|
3002
3237
|
$meshCounter
|
|
3003
3238
|
};
|
|
3004
3239
|
|
|
3005
|
-
//# debugId=
|
|
3240
|
+
//# debugId=15751E988ED47F0264756E2164756E21
|