@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.
@@ -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
- 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
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.createInitiatingSlot(remotePeerId);
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.createInitiatingSlot(remotePeerId);
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.createInitiatingSlot(remotePeerId);
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 false;
2241
+ return "self";
2076
2242
  if (!this.hasKnownPeer(remotePeerId))
2077
- return false;
2243
+ return "not-in-keyring";
2244
+ if (!this.presentPeers.has(remotePeerId))
2245
+ return "not-present";
2078
2246
  if (this.slots.has(remotePeerId))
2079
- return false;
2247
+ return "slot-already-exists";
2080
2248
  if (this.localPeerId <= remotePeerId)
2081
- return false;
2082
- return true;
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.refreshKnownPeers();
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({ iceServers: this.iceServers });
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
- this.signaling.sendSignal(targetId, { kind: "offer", sdp: offer });
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({ iceServers: this.iceServers });
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: answer
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
- this.signaling.sendSignal(peerId, {
2276
- kind: "ice",
2277
- candidate: event.candidate.toJSON()
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.emit("peer-candidate", {
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=18409B674B254AA864756E2164756E21
3333
+ //# debugId=27D9577FC215E14564756E2164756E21