@fairfox/polly 0.55.0 → 0.57.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.
@@ -21,7 +21,7 @@ export type { EncryptedEnvelope, SealedBytes, } from "./shared/lib/encryption";
21
21
  export { decrypt, decryptOrThrow, EncryptionError, encrypt, generateDocumentKey, KEY_BYTES as ENCRYPTION_KEY_BYTES, NONCE_BYTES as ENCRYPTION_NONCE_BYTES, TAG_BYTES as ENCRYPTION_TAG_BYTES, } from "./shared/lib/encryption";
22
22
  export type { KeyringStorage } from "./shared/lib/keyring-storage";
23
23
  export { deserialiseKeyring, memoryKeyringStorage, serialiseKeyring, } from "./shared/lib/keyring-storage";
24
- export type { CreateMeshClientOptions, MeshClient } from "./shared/lib/mesh-client";
24
+ export type { CreateMeshClientOptions, MeshClient, MeshClientHandleSnapshot, MeshClientPeerStateSnapshot, } from "./shared/lib/mesh-client";
25
25
  export { createMeshClient } from "./shared/lib/mesh-client";
26
26
  export type { MeshKeyring, MeshNetworkAdapterOptions, } from "./shared/lib/mesh-network-adapter";
27
27
  export { DEFAULT_MESH_KEY_ID, MeshNetworkAdapter, } from "./shared/lib/mesh-network-adapter";
@@ -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 { HandleSyncSnapshot, 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
@@ -1075,12 +1075,16 @@ class MeshNetworkAdapter extends NetworkAdapter {
1075
1075
  }
1076
1076
  const signed = signEnvelope(payloadToSign, message.senderId, keyring.identity.secretKey);
1077
1077
  const signedBytes = encodeSignedEnvelope(signed);
1078
- return {
1078
+ const outer = {
1079
1079
  type: message.type,
1080
1080
  senderId: message.senderId,
1081
1081
  targetId: message.targetId,
1082
1082
  data: signedBytes
1083
1083
  };
1084
+ if ("documentId" in message && message.documentId !== undefined) {
1085
+ outer["documentId"] = message.documentId;
1086
+ }
1087
+ return outer;
1084
1088
  }
1085
1089
  tryUnwrap(message) {
1086
1090
  if (!message.data)
@@ -2033,6 +2037,10 @@ function selectActivePair(parsed) {
2033
2037
  return;
2034
2038
  }
2035
2039
  function serialiseSlotView(slot) {
2040
+ const handles = {};
2041
+ for (const [docId, snapshot] of slot.handles) {
2042
+ handles[docId] = { ...snapshot };
2043
+ }
2036
2044
  return {
2037
2045
  signalingState: slot.connection.signalingState,
2038
2046
  iceConnectionState: slot.connection.iceConnectionState,
@@ -2044,7 +2052,17 @@ function serialiseSlotView(slot) {
2044
2052
  transport: slot.transport ? {
2045
2053
  ...slot.transport,
2046
2054
  selectedCandidatePair: slot.transport.selectedCandidatePair ? { ...slot.transport.selectedCandidatePair } : undefined
2047
- } : undefined
2055
+ } : undefined,
2056
+ lastSyncHandshakeAttempt: { ...slot.lastSyncHandshakeAttempt },
2057
+ handles
2058
+ };
2059
+ }
2060
+ function emptySyncHandshakeAttempt() {
2061
+ return {
2062
+ dataChannelOpenedAt: undefined,
2063
+ peerCandidateEmittedAt: undefined,
2064
+ firstOutboundSendAt: undefined,
2065
+ firstInboundMessageAt: undefined
2048
2066
  };
2049
2067
  }
2050
2068
  function buildCandidatePairView(pair, localCands, remoteCands) {
@@ -2063,6 +2081,14 @@ var DEFAULT_ICE_SERVERS = [
2063
2081
  { urls: "stun:stun.l.google.com:19302" },
2064
2082
  { urls: "stun:stun1.l.google.com:19302" }
2065
2083
  ];
2084
+ function emptyHandleSyncSnapshot() {
2085
+ return {
2086
+ lastSyncMessageOutAt: undefined,
2087
+ lastSyncMessageInAt: undefined,
2088
+ lastSyncMessageOutSize: undefined,
2089
+ lastSyncMessageOutType: undefined
2090
+ };
2091
+ }
2066
2092
 
2067
2093
  class MeshWebRTCAdapter extends NetworkAdapter2 {
2068
2094
  signaling;
@@ -2082,6 +2108,9 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2082
2108
  slots = new Map;
2083
2109
  ready = false;
2084
2110
  readyResolver;
2111
+ lastSlotInitiationDecisions = new Map;
2112
+ sweepRunCount = 0;
2113
+ lastSweepAt;
2085
2114
  get knownPeerIds() {
2086
2115
  if (this.keyringSource !== undefined) {
2087
2116
  return [...this.keyringSource().knownPeers.keys()].filter((id) => id !== this.localPeerId);
@@ -2134,10 +2163,13 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2134
2163
  const peers = [];
2135
2164
  for (const peerId of allPeers) {
2136
2165
  const slot = this.slots.get(peerId);
2166
+ const decision = this.snapshotInitiationDecision(peerId);
2137
2167
  peers.push({
2138
2168
  peerId,
2139
2169
  knownInKeyring: knownPeerSet.has(peerId),
2140
2170
  presentInSignalling: this.presentPeers.has(peerId),
2171
+ slotInitiationRejectionReason: decision.reason,
2172
+ slotInitiationDecision: decision,
2141
2173
  slot: slot ? serialiseSlotView(slot) : undefined
2142
2174
  });
2143
2175
  }
@@ -2145,6 +2177,12 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2145
2177
  localPeerId: this.localPeerId,
2146
2178
  knownPeerIds,
2147
2179
  presentPeerIds,
2180
+ sweep: {
2181
+ enabled: this.knownPeersRefreshTimer !== undefined,
2182
+ intervalMs: this.knownPeersRefreshIntervalMs,
2183
+ runCount: this.sweepRunCount,
2184
+ lastRunAt: this.lastSweepAt
2185
+ },
2148
2186
  peers
2149
2187
  };
2150
2188
  }
@@ -2152,14 +2190,27 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2152
2190
  this.presentPeers.add(remotePeerId);
2153
2191
  if (!this.shouldInitiateTo(remotePeerId))
2154
2192
  return;
2155
- this.createInitiatingSlot(remotePeerId);
2193
+ this.tryCreateInitiatingSlot(remotePeerId);
2156
2194
  }
2157
2195
  handlePeersPresent(peerIds) {
2158
2196
  for (const remotePeerId of peerIds) {
2159
2197
  this.presentPeers.add(remotePeerId);
2160
2198
  if (!this.shouldInitiateTo(remotePeerId))
2161
2199
  continue;
2200
+ this.tryCreateInitiatingSlot(remotePeerId);
2201
+ }
2202
+ }
2203
+ tryCreateInitiatingSlot(remotePeerId) {
2204
+ try {
2162
2205
  this.createInitiatingSlot(remotePeerId);
2206
+ } catch (err) {
2207
+ const message = err instanceof Error ? err.message : String(err);
2208
+ this.lastSlotInitiationDecisions.set(remotePeerId, {
2209
+ decision: "rejected",
2210
+ reason: "fatal-error",
2211
+ error: message,
2212
+ at: performance.now()
2213
+ });
2163
2214
  }
2164
2215
  }
2165
2216
  handlePeerLeft(remotePeerId) {
@@ -2183,25 +2234,49 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2183
2234
  return;
2184
2235
  if (!this.shouldInitiateTo(remotePeerId))
2185
2236
  return;
2186
- this.createInitiatingSlot(remotePeerId);
2237
+ this.tryCreateInitiatingSlot(remotePeerId);
2187
2238
  }
2188
2239
  refreshKnownPeers() {
2189
2240
  for (const remotePeerId of this.presentPeers) {
2190
2241
  if (!this.shouldInitiateTo(remotePeerId))
2191
2242
  continue;
2192
- this.createInitiatingSlot(remotePeerId);
2243
+ this.tryCreateInitiatingSlot(remotePeerId);
2193
2244
  }
2194
2245
  }
2195
2246
  shouldInitiateTo(remotePeerId) {
2247
+ const reason = this.evaluateInitiation(remotePeerId);
2248
+ this.lastSlotInitiationDecisions.set(remotePeerId, {
2249
+ decision: reason === undefined ? "accepted" : "rejected",
2250
+ reason,
2251
+ error: undefined,
2252
+ at: performance.now()
2253
+ });
2254
+ return reason === undefined;
2255
+ }
2256
+ evaluateInitiation(remotePeerId) {
2196
2257
  if (remotePeerId === this.localPeerId)
2197
- return false;
2258
+ return "self";
2198
2259
  if (!this.hasKnownPeer(remotePeerId))
2199
- return false;
2260
+ return "not-in-keyring";
2261
+ if (!this.presentPeers.has(remotePeerId))
2262
+ return "not-present";
2200
2263
  if (this.slots.has(remotePeerId))
2201
- return false;
2264
+ return "slot-already-exists";
2202
2265
  if (this.localPeerId <= remotePeerId)
2203
- return false;
2204
- return true;
2266
+ return "tie-break-other-side";
2267
+ return;
2268
+ }
2269
+ snapshotInitiationDecision(remotePeerId) {
2270
+ const cached = this.lastSlotInitiationDecisions.get(remotePeerId);
2271
+ if (cached?.reason === "fatal-error")
2272
+ return cached;
2273
+ const reason = this.evaluateInitiation(remotePeerId);
2274
+ return {
2275
+ decision: reason === undefined ? "accepted" : "rejected",
2276
+ reason,
2277
+ error: undefined,
2278
+ at: performance.now()
2279
+ };
2205
2280
  }
2206
2281
  whenReady() {
2207
2282
  if (this.ready)
@@ -2238,7 +2313,11 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2238
2313
  if (this.knownPeersRefreshTimer !== undefined)
2239
2314
  return;
2240
2315
  this.knownPeersRefreshTimer = setInterval(() => {
2241
- this.refreshKnownPeers();
2316
+ this.sweepRunCount += 1;
2317
+ this.lastSweepAt = performance.now();
2318
+ try {
2319
+ this.refreshKnownPeers();
2320
+ } catch {}
2242
2321
  }, this.knownPeersRefreshIntervalMs);
2243
2322
  }
2244
2323
  stopKnownPeersSweep() {
@@ -2254,6 +2333,20 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2254
2333
  if (!slot) {
2255
2334
  slot = this.createInitiatingSlot(targetId);
2256
2335
  }
2336
+ if (slot.lastSyncHandshakeAttempt.firstOutboundSendAt === undefined) {
2337
+ slot.lastSyncHandshakeAttempt.firstOutboundSendAt = performance.now();
2338
+ }
2339
+ const documentId = message.documentId;
2340
+ if (typeof documentId === "string") {
2341
+ let handleEntry = slot.handles.get(documentId);
2342
+ if (!handleEntry) {
2343
+ handleEntry = emptyHandleSyncSnapshot();
2344
+ slot.handles.set(documentId, handleEntry);
2345
+ }
2346
+ handleEntry.lastSyncMessageOutAt = performance.now();
2347
+ handleEntry.lastSyncMessageOutSize = bytes.length;
2348
+ handleEntry.lastSyncMessageOutType = typeof message.type === "string" ? message.type : undefined;
2349
+ }
2257
2350
  if (slot.channel && slot.channel.readyState === "open") {
2258
2351
  this.sendBytesMaybeFragmented(slot.channel, bytes);
2259
2352
  } else {
@@ -2366,7 +2459,9 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2366
2459
  pendingRemoteIce: [],
2367
2460
  inFlightSync: undefined,
2368
2461
  transport: undefined,
2369
- lastDataChannelError: undefined
2462
+ lastDataChannelError: undefined,
2463
+ lastSyncHandshakeAttempt: emptySyncHandshakeAttempt(),
2464
+ handles: new Map
2370
2465
  };
2371
2466
  this.slots.set(targetId, slot);
2372
2467
  this.wireConnection(targetId, connection);
@@ -2407,7 +2502,9 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2407
2502
  pendingRemoteIce: [],
2408
2503
  inFlightSync: undefined,
2409
2504
  transport: undefined,
2410
- lastDataChannelError: undefined
2505
+ lastDataChannelError: undefined,
2506
+ lastSyncHandshakeAttempt: emptySyncHandshakeAttempt(),
2507
+ handles: new Map
2411
2508
  };
2412
2509
  this.slots.set(fromPeerId, slot);
2413
2510
  this.wireConnection(fromPeerId, connection);
@@ -2477,21 +2574,32 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2477
2574
  connection.onconnectionstatechange = () => {
2478
2575
  const state = connection.connectionState;
2479
2576
  if (state === "connected") {
2480
- this.emit("peer-candidate", {
2481
- peerId,
2482
- peerMetadata: {}
2483
- });
2577
+ this.emitPeerCandidateOnce(peerId);
2484
2578
  } else if (state === "disconnected" || state === "failed" || state === "closed") {
2485
2579
  this.slots.delete(peerId);
2486
2580
  this.emit("peer-disconnected", { peerId });
2487
2581
  }
2488
2582
  };
2489
2583
  }
2584
+ emitPeerCandidateOnce(peerId) {
2585
+ const slot = this.slots.get(peerId);
2586
+ if (!slot)
2587
+ return;
2588
+ if (slot.lastSyncHandshakeAttempt.peerCandidateEmittedAt !== undefined)
2589
+ return;
2590
+ slot.lastSyncHandshakeAttempt.peerCandidateEmittedAt = performance.now();
2591
+ this.emit("peer-candidate", {
2592
+ peerId,
2593
+ peerMetadata: {}
2594
+ });
2595
+ }
2490
2596
  wireDataChannel(peerId, channel) {
2491
2597
  channel.onopen = () => {
2492
2598
  const slot = this.slots.get(peerId);
2493
2599
  if (!slot)
2494
2600
  return;
2601
+ slot.lastSyncHandshakeAttempt.dataChannelOpenedAt = performance.now();
2602
+ this.emitPeerCandidateOnce(peerId);
2495
2603
  for (const bytes of slot.pendingSends) {
2496
2604
  this.sendBytesMaybeFragmented(channel, bytes);
2497
2605
  }
@@ -2559,6 +2667,8 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2559
2667
  } catch {}
2560
2668
  }
2561
2669
  scheduleEmitMessage(fromPeerId, message, viaFragmentPath) {
2670
+ this.stampFirstInboundMessage(fromPeerId);
2671
+ this.stampHandleInbound(fromPeerId, message);
2562
2672
  if (!this.syncYieldEnabled) {
2563
2673
  this.emit("message", message);
2564
2674
  if (viaFragmentPath) {
@@ -2582,6 +2692,28 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2582
2692
  }
2583
2693
  }, 0);
2584
2694
  }
2695
+ stampFirstInboundMessage(fromPeerId) {
2696
+ const slot = this.slots.get(fromPeerId);
2697
+ if (!slot)
2698
+ return;
2699
+ if (slot.lastSyncHandshakeAttempt.firstInboundMessageAt !== undefined)
2700
+ return;
2701
+ slot.lastSyncHandshakeAttempt.firstInboundMessageAt = performance.now();
2702
+ }
2703
+ stampHandleInbound(fromPeerId, message) {
2704
+ const documentId = message.documentId;
2705
+ if (typeof documentId !== "string")
2706
+ return;
2707
+ const slot = this.slots.get(fromPeerId);
2708
+ if (!slot)
2709
+ return;
2710
+ let entry = slot.handles.get(documentId);
2711
+ if (!entry) {
2712
+ entry = emptyHandleSyncSnapshot();
2713
+ slot.handles.set(documentId, entry);
2714
+ }
2715
+ entry.lastSyncMessageInAt = performance.now();
2716
+ }
2585
2717
  finishInFlightSyncApply(fromPeerId) {
2586
2718
  const slot = this.slots.get(fromPeerId);
2587
2719
  if (!slot?.inFlightSync)
@@ -2723,6 +2855,54 @@ async function resolveIceServers(rtc) {
2723
2855
  }
2724
2856
  return rtc?.iceServers;
2725
2857
  }
2858
+ function buildHandleEntry(state, wire) {
2859
+ return {
2860
+ state,
2861
+ announcedToPeer: wire?.lastSyncMessageOutAt !== undefined,
2862
+ lastSyncMessageOutAt: wire?.lastSyncMessageOutAt,
2863
+ lastSyncMessageInAt: wire?.lastSyncMessageInAt,
2864
+ lastSyncMessageOutSize: wire?.lastSyncMessageOutSize,
2865
+ lastSyncMessageOutType: wire?.lastSyncMessageOutType
2866
+ };
2867
+ }
2868
+ function stringifyHandleState(handle) {
2869
+ if (handle === undefined)
2870
+ return "unknown";
2871
+ return typeof handle.state === "string" ? handle.state : String(handle.state ?? "unknown");
2872
+ }
2873
+ function enrichPeerSlot(peer, knownHandleIds, repoHandles) {
2874
+ if (!peer.slot) {
2875
+ return { ...peer, slot: undefined };
2876
+ }
2877
+ const enriched = {};
2878
+ for (const docId of knownHandleIds) {
2879
+ enriched[docId] = buildHandleEntry(stringifyHandleState(repoHandles[docId]), peer.slot.handles[docId]);
2880
+ }
2881
+ for (const docId of Object.keys(peer.slot.handles)) {
2882
+ if (enriched[docId])
2883
+ continue;
2884
+ enriched[docId] = buildHandleEntry("unknown", peer.slot.handles[docId]);
2885
+ }
2886
+ return { ...peer, slot: { ...peer.slot, handles: enriched } };
2887
+ }
2888
+ function getReevaluateDocumentShare(repo) {
2889
+ const sync = repo.synchronizer;
2890
+ const fn = sync?.reevaluateDocumentShare;
2891
+ if (typeof fn !== "function" || sync === undefined)
2892
+ return;
2893
+ return () => fn.call(sync);
2894
+ }
2895
+ function installPolly107SyncReevaluation(networkAdapter, repo) {
2896
+ const disable = typeof process !== "undefined" && process.env?.["POLLY_107_DISABLE_FIX"] === "1";
2897
+ if (disable)
2898
+ return;
2899
+ const reevaluate = getReevaluateDocumentShare(repo);
2900
+ if (!reevaluate)
2901
+ return;
2902
+ networkAdapter.on("peer-candidate", () => {
2903
+ reevaluate().catch(() => {});
2904
+ });
2905
+ }
2726
2906
  async function createMeshClient(options) {
2727
2907
  const keyringSource = await resolveKeyringSource(options.keyring);
2728
2908
  const keyring = keyringSource();
@@ -2795,6 +2975,7 @@ async function createMeshClient(options) {
2795
2975
  ...options.repoStorage !== undefined && { storage: options.repoStorage }
2796
2976
  });
2797
2977
  configureMeshState(repo);
2978
+ installPolly107SyncReevaluation(networkAdapter, repo);
2798
2979
  await signaling.connect();
2799
2980
  return {
2800
2981
  repo,
@@ -2813,10 +2994,33 @@ async function createMeshClient(options) {
2813
2994
  localPeerId: options.signaling.peerId,
2814
2995
  knownPeerIds: [],
2815
2996
  presentPeerIds: [],
2997
+ sweep: {
2998
+ enabled: false,
2999
+ intervalMs: 0,
3000
+ runCount: 0,
3001
+ lastRunAt: undefined
3002
+ },
2816
3003
  peers: []
2817
3004
  };
2818
3005
  }
2819
- return webrtcAdapter.getPeerStateSnapshot();
3006
+ const base = webrtcAdapter.getPeerStateSnapshot();
3007
+ const repoHandles = repo.handles;
3008
+ const knownHandleIds = Object.keys(repoHandles);
3009
+ const enrichedPeers = base.peers.map((peer) => enrichPeerSlot(peer, knownHandleIds, repoHandles));
3010
+ const out = {
3011
+ localPeerId: base.localPeerId,
3012
+ knownPeerIds: base.knownPeerIds,
3013
+ presentPeerIds: base.presentPeerIds,
3014
+ sweep: base.sweep,
3015
+ peers: enrichedPeers
3016
+ };
3017
+ return out;
3018
+ },
3019
+ reevaluateAllSync: async () => {
3020
+ const reevaluate = getReevaluateDocumentShare(repo);
3021
+ if (!reevaluate)
3022
+ return;
3023
+ await reevaluate();
2820
3024
  },
2821
3025
  refreshTransportStats: async () => {
2822
3026
  if (!webrtcAdapter)
@@ -3237,4 +3441,4 @@ export {
3237
3441
  $meshCounter
3238
3442
  };
3239
3443
 
3240
- //# debugId=15751E988ED47F0264756E2164756E21
3444
+ //# debugId=DAB92455464EE0D864756E2164756E21