@fairfox/polly 0.64.0 → 0.65.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
@@ -2364,7 +2364,9 @@ function serialiseSlotView(slot) {
2364
2364
  selectedCandidatePair: slot.transport.selectedCandidatePair ? { ...slot.transport.selectedCandidatePair } : undefined
2365
2365
  } : undefined,
2366
2366
  lastSyncHandshakeAttempt: { ...slot.lastSyncHandshakeAttempt },
2367
- handles
2367
+ handles,
2368
+ createdAt: slot.createdAt,
2369
+ lastInboundAt: slot.lastInboundAt
2368
2370
  };
2369
2371
  }
2370
2372
  function emptySyncHandshakeAttempt() {
@@ -2391,6 +2393,9 @@ var DEFAULT_ICE_SERVERS = [
2391
2393
  { urls: "stun:stun.l.google.com:19302" },
2392
2394
  { urls: "stun:stun1.l.google.com:19302" }
2393
2395
  ];
2396
+ var SLOT_NEVER_CONNECTED_TIMEOUT_MS = 30000;
2397
+ var SLOT_IDLE_TIMEOUT_MS = 120000;
2398
+ var SLOT_WATCHDOG_INTERVAL_MS = 5000;
2394
2399
  function emptyHandleSyncSnapshot() {
2395
2400
  return {
2396
2401
  lastSyncMessageOutAt: undefined,
@@ -2421,6 +2426,10 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2421
2426
  lastSlotInitiationDecisions = new Map;
2422
2427
  sweepRunCount = 0;
2423
2428
  lastSweepAt;
2429
+ slotWatchdogTimer;
2430
+ slotNeverConnectedTimeoutMs;
2431
+ slotIdleTimeoutMs;
2432
+ slotWatchdogIntervalMs;
2424
2433
  get knownPeerIds() {
2425
2434
  if (this.keyringSource !== undefined) {
2426
2435
  return [...this.keyringSource().knownPeers.keys()].filter((id) => id !== this.localPeerId);
@@ -2446,6 +2455,9 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2446
2455
  this.knownPeersRefreshIntervalMs = options.knownPeersRefreshIntervalMs ?? 2000;
2447
2456
  this.syncYieldEnabled = options.syncYieldEnabled ?? true;
2448
2457
  this.syncFragmentChunkSize = options.syncFragmentChunkSizeOverride ?? SYNC_FRAGMENT_CHUNK_SIZE;
2458
+ this.slotNeverConnectedTimeoutMs = options.slotNeverConnectedTimeoutMs ?? SLOT_NEVER_CONNECTED_TIMEOUT_MS;
2459
+ this.slotIdleTimeoutMs = options.slotIdleTimeoutMs ?? SLOT_IDLE_TIMEOUT_MS;
2460
+ this.slotWatchdogIntervalMs = options.slotWatchdogIntervalMs ?? SLOT_WATCHDOG_INTERVAL_MS;
2449
2461
  this.localPeerId = options.peerId;
2450
2462
  const PC = options.RTCPeerConnection ?? globalThis.RTCPeerConnection;
2451
2463
  if (typeof PC !== "function") {
@@ -2603,9 +2615,11 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2603
2615
  this.ready = true;
2604
2616
  this.readyResolver?.();
2605
2617
  this.startKnownPeersSweep();
2618
+ this.startSlotWatchdog();
2606
2619
  }
2607
2620
  disconnect() {
2608
2621
  this.stopKnownPeersSweep();
2622
+ this.stopSlotWatchdog();
2609
2623
  for (const slot of this.slots.values()) {
2610
2624
  slot.channel?.close();
2611
2625
  slot.connection.close();
@@ -2636,6 +2650,70 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2636
2650
  clearInterval(this.knownPeersRefreshTimer);
2637
2651
  this.knownPeersRefreshTimer = undefined;
2638
2652
  }
2653
+ tearDownWedgedSlot(peerId) {
2654
+ const slot = this.slots.get(peerId);
2655
+ if (!slot)
2656
+ return;
2657
+ this.slots.delete(peerId);
2658
+ try {
2659
+ slot.channel?.close();
2660
+ } catch {}
2661
+ try {
2662
+ slot.connection.close();
2663
+ } catch {}
2664
+ this.emit("peer-disconnected", { peerId });
2665
+ }
2666
+ startSlotWatchdog() {
2667
+ if (this.slotWatchdogIntervalMs <= 0)
2668
+ return;
2669
+ if (this.slotWatchdogTimer !== undefined)
2670
+ return;
2671
+ this.slotWatchdogTimer = setInterval(() => {
2672
+ try {
2673
+ this.sweepWedgedSlots();
2674
+ } catch {}
2675
+ }, this.slotWatchdogIntervalMs);
2676
+ }
2677
+ stopSlotWatchdog() {
2678
+ if (this.slotWatchdogTimer === undefined)
2679
+ return;
2680
+ clearInterval(this.slotWatchdogTimer);
2681
+ this.slotWatchdogTimer = undefined;
2682
+ }
2683
+ sweepWedgedSlots() {
2684
+ const now = performance.now();
2685
+ const peerIds = [...this.slots.keys()];
2686
+ for (const peerId of peerIds) {
2687
+ const slot = this.slots.get(peerId);
2688
+ if (!slot)
2689
+ continue;
2690
+ const reason = this.classifyWedgedSlot(slot, now);
2691
+ if (!reason)
2692
+ continue;
2693
+ this.lastSlotInitiationDecisions.set(peerId, {
2694
+ decision: "rejected",
2695
+ reason: "fatal-error",
2696
+ error: reason,
2697
+ at: now
2698
+ });
2699
+ this.tearDownWedgedSlot(peerId);
2700
+ }
2701
+ }
2702
+ classifyWedgedSlot(slot, now) {
2703
+ const state = slot.connection.connectionState;
2704
+ const ageMs = now - slot.createdAt;
2705
+ if (this.slotNeverConnectedTimeoutMs > 0 && (state === "new" || state === "connecting") && ageMs > this.slotNeverConnectedTimeoutMs) {
2706
+ return `slot never reached connected (state=${state}) after ${Math.round(ageMs)}ms`;
2707
+ }
2708
+ if (this.slotIdleTimeoutMs > 0 && state === "connected" && slot.channel?.readyState === "open") {
2709
+ const lastInbound = slot.lastInboundAt ?? slot.createdAt;
2710
+ const idleMs = now - lastInbound;
2711
+ if (idleMs > this.slotIdleTimeoutMs) {
2712
+ return `slot idle: no inbound bytes for ${Math.round(idleMs)}ms (state=connected, dc=open)`;
2713
+ }
2714
+ }
2715
+ return;
2716
+ }
2639
2717
  send(message) {
2640
2718
  const targetId = message.targetId;
2641
2719
  const bytes = this.serialiseMessage(message);
@@ -2771,12 +2849,23 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2771
2849
  transport: undefined,
2772
2850
  lastDataChannelError: undefined,
2773
2851
  lastSyncHandshakeAttempt: emptySyncHandshakeAttempt(),
2774
- handles: new Map
2852
+ handles: new Map,
2853
+ createdAt: performance.now(),
2854
+ lastInboundAt: undefined
2775
2855
  };
2776
2856
  this.slots.set(targetId, slot);
2777
2857
  this.wireConnection(targetId, connection);
2778
2858
  this.wireDataChannel(targetId, channel);
2779
- this.initiateOffer(targetId, connection);
2859
+ this.initiateOffer(targetId, connection).catch((err) => {
2860
+ const message = err instanceof Error ? err.message : String(err);
2861
+ this.lastSlotInitiationDecisions.set(targetId, {
2862
+ decision: "rejected",
2863
+ reason: "fatal-error",
2864
+ error: message,
2865
+ at: performance.now()
2866
+ });
2867
+ this.tearDownWedgedSlot(targetId);
2868
+ });
2780
2869
  return slot;
2781
2870
  }
2782
2871
  async initiateOffer(targetId, connection) {
@@ -2814,7 +2903,9 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2814
2903
  transport: undefined,
2815
2904
  lastDataChannelError: undefined,
2816
2905
  lastSyncHandshakeAttempt: emptySyncHandshakeAttempt(),
2817
- handles: new Map
2906
+ handles: new Map,
2907
+ createdAt: performance.now(),
2908
+ lastInboundAt: undefined
2818
2909
  };
2819
2910
  this.slots.set(fromPeerId, slot);
2820
2911
  this.wireConnection(fromPeerId, connection);
@@ -2886,6 +2977,8 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2886
2977
  if (state === "connected") {
2887
2978
  this.emitPeerCandidateOnce(peerId);
2888
2979
  } else if (state === "disconnected" || state === "failed" || state === "closed") {
2980
+ if (!this.slots.has(peerId))
2981
+ return;
2889
2982
  this.slots.delete(peerId);
2890
2983
  this.emit("peer-disconnected", { peerId });
2891
2984
  }
@@ -2916,6 +3009,9 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2916
3009
  slot.pendingSends = [];
2917
3010
  };
2918
3011
  channel.onmessage = (event) => {
3012
+ const liveSlot = this.slots.get(peerId);
3013
+ if (liveSlot)
3014
+ liveSlot.lastInboundAt = performance.now();
2919
3015
  const data = event.data;
2920
3016
  if (data instanceof ArrayBuffer) {
2921
3017
  this.dispatchMessage(peerId, new Uint8Array(data));
@@ -3807,4 +3903,4 @@ export {
3807
3903
  $meshCounter
3808
3904
  };
3809
3905
 
3810
- //# debugId=32FAEA2DE6283FD764756E2164756E21
3906
+ //# debugId=9FE5660EB3793A1964756E2164756E21