@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({ iceServers: this.iceServers });
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
- this.signaling.sendSignal(targetId, { kind: "offer", sdp: offer });
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({ iceServers: this.iceServers });
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: answer
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
- this.signaling.sendSignal(peerId, {
2276
- kind: "ice",
2277
- candidate: event.candidate.toJSON()
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=18409B674B254AA864756E2164756E21
3240
+ //# debugId=15751E988ED47F0264756E2164756E21