@fairfox/polly 0.64.0 → 0.66.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));
@@ -3165,14 +3261,49 @@ async function resolveIceServers(rtc) {
3165
3261
  }
3166
3262
  return rtc?.iceServers;
3167
3263
  }
3168
- function buildHandleEntry(state, wire) {
3264
+ function buildHandleEntry(state, wire, syncStateView) {
3169
3265
  return {
3170
3266
  state,
3171
3267
  announcedToPeer: wire?.lastSyncMessageOutAt !== undefined,
3172
3268
  lastSyncMessageOutAt: wire?.lastSyncMessageOutAt,
3173
3269
  lastSyncMessageInAt: wire?.lastSyncMessageInAt,
3174
3270
  lastSyncMessageOutSize: wire?.lastSyncMessageOutSize,
3175
- lastSyncMessageOutType: wire?.lastSyncMessageOutType
3271
+ lastSyncMessageOutType: wire?.lastSyncMessageOutType,
3272
+ docSynchronizerExists: syncStateView.docSynchronizerExists,
3273
+ docSynchronizerKnowsPeer: syncStateView.docSynchronizerKnowsPeer,
3274
+ peerDocumentStatus: syncStateView.peerDocumentStatus
3275
+ };
3276
+ }
3277
+ var EMPTY_SYNC_VIEW = {
3278
+ docSynchronizerExists: false,
3279
+ docSynchronizerKnowsPeer: undefined,
3280
+ peerDocumentStatus: undefined
3281
+ };
3282
+ function getCollectionSynchronizer(repo) {
3283
+ const sync = repo.synchronizer;
3284
+ return sync && typeof sync === "object" ? sync : undefined;
3285
+ }
3286
+ function buildSyncView(synchronizer, docId, peerId) {
3287
+ const docSync = synchronizer?.docSynchronizers?.[docId];
3288
+ if (!docSync)
3289
+ return EMPTY_SYNC_VIEW;
3290
+ let knowsPeer;
3291
+ try {
3292
+ knowsPeer = typeof docSync.hasPeer === "function" ? docSync.hasPeer(peerId) : undefined;
3293
+ } catch {
3294
+ knowsPeer = undefined;
3295
+ }
3296
+ let status;
3297
+ try {
3298
+ const states = docSync.peerStates;
3299
+ status = states && typeof states === "object" ? states[peerId] : undefined;
3300
+ } catch {
3301
+ status = undefined;
3302
+ }
3303
+ return {
3304
+ docSynchronizerExists: true,
3305
+ docSynchronizerKnowsPeer: knowsPeer,
3306
+ peerDocumentStatus: status
3176
3307
  };
3177
3308
  }
3178
3309
  function stringifyHandleState(handle) {
@@ -3180,18 +3311,19 @@ function stringifyHandleState(handle) {
3180
3311
  return "unknown";
3181
3312
  return typeof handle.state === "string" ? handle.state : String(handle.state ?? "unknown");
3182
3313
  }
3183
- function enrichPeerSlot(peer, knownHandleIds, repoHandles) {
3314
+ function enrichPeerSlot(peer, knownHandleIds, repoHandles, synchronizer) {
3184
3315
  if (!peer.slot) {
3185
3316
  return { ...peer, slot: undefined };
3186
3317
  }
3318
+ const peerIdString = peer.peerId;
3187
3319
  const enriched = {};
3188
3320
  for (const docId of knownHandleIds) {
3189
- enriched[docId] = buildHandleEntry(stringifyHandleState(repoHandles[docId]), peer.slot.handles[docId]);
3321
+ enriched[docId] = buildHandleEntry(stringifyHandleState(repoHandles[docId]), peer.slot.handles[docId], buildSyncView(synchronizer, docId, peerIdString));
3190
3322
  }
3191
3323
  for (const docId of Object.keys(peer.slot.handles)) {
3192
3324
  if (enriched[docId])
3193
3325
  continue;
3194
- enriched[docId] = buildHandleEntry("unknown", peer.slot.handles[docId]);
3326
+ enriched[docId] = buildHandleEntry("unknown", peer.slot.handles[docId], buildSyncView(synchronizer, docId, peerIdString));
3195
3327
  }
3196
3328
  return { ...peer, slot: { ...peer.slot, handles: enriched } };
3197
3329
  }
@@ -3353,7 +3485,8 @@ async function createMeshClient(options) {
3353
3485
  const base = webrtcAdapter.getPeerStateSnapshot();
3354
3486
  const repoHandles = repo.handles;
3355
3487
  const knownHandleIds = Object.keys(repoHandles);
3356
- const enrichedPeers = base.peers.map((peer) => enrichPeerSlot(peer, knownHandleIds, repoHandles));
3488
+ const synchronizer = getCollectionSynchronizer(repo);
3489
+ const enrichedPeers = base.peers.map((peer) => enrichPeerSlot(peer, knownHandleIds, repoHandles, synchronizer));
3357
3490
  const out = {
3358
3491
  localPeerId: base.localPeerId,
3359
3492
  knownPeerIds: base.knownPeerIds,
@@ -3807,4 +3940,4 @@ export {
3807
3940
  $meshCounter
3808
3941
  };
3809
3942
 
3810
- //# debugId=32FAEA2DE6283FD764756E2164756E21
3943
+ //# debugId=E998FF56BA12701D64756E2164756E21