@fairfox/polly 0.63.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.
@@ -28,7 +28,7 @@ export { DEFAULT_MESH_KEY_ID, MeshNetworkAdapter, } from "./shared/lib/mesh-netw
28
28
  export type { MeshSignalingClientOptions, SignalingMessage as MeshSignalingMessage, } from "./shared/lib/mesh-signaling-client";
29
29
  export { MeshSignalingClient } from "./shared/lib/mesh-signaling-client";
30
30
  export type { DocIdResolver, LazyWrapperExitReason, MeshStateLazyWrapperRecord, MeshStateLoadedRejectionBreadcrumb, MeshStateOptions, MeshStateStorageOpenError, } from "./shared/lib/mesh-state";
31
- export { $meshCounter, $meshList, $meshState, $meshText, configureMeshState, deriveDocumentId, getDocIdResolver, getLastConfiguredRepoPeerId, getLastLoadedRejection, getLazyInvocations, getLazyReachedRepo, getLazyWrappers, getMeshStateModuleId, getStorageOpenError, isMeshStateConfigured, MESH_STATE_MODULE_ID, registerDocIdResolver, resetMeshState, resolveDocumentId, wasMeshStateResolved, } from "./shared/lib/mesh-state";
31
+ export { $meshCounter, $meshList, $meshState, $meshText, configureMeshState, deriveDocumentId, getDocIdResolver, getLastConfiguredRepoPeerId, getLastLoadedRejection, getLazyInvocations, getLazyReachedRepo, getLazyWrappers, getMeshStateModuleId, getRedirectDetector, getStorageOpenError, isMeshStateConfigured, MESH_STATE_MODULE_ID, registerDocIdResolver, registerRedirectDetector, resetMeshState, resolveDocumentId, wasMeshStateResolved, } from "./shared/lib/mesh-state";
32
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";
package/dist/src/mesh.js CHANGED
@@ -1729,25 +1729,10 @@ function $crdtState(options) {
1729
1729
  const inner = signal2(options.initialValue);
1730
1730
  let updating = false;
1731
1731
  let currentHandle;
1732
- const loaded = (async () => {
1733
- const handle = await options.getHandle();
1734
- await handle.whenReady();
1735
- currentHandle = handle;
1736
- if (options.schemaVersion !== undefined) {
1737
- const targetVersion = options.schemaVersion;
1738
- const migrations = options.migrations ?? {};
1739
- handle.change((doc) => {
1740
- runMigrations(doc, targetVersion, migrations);
1741
- setDocVersion(doc, targetVersion);
1742
- });
1743
- }
1744
- updating = true;
1745
- try {
1746
- inner.value = cloneDoc(handle.doc());
1747
- } finally {
1748
- updating = false;
1749
- }
1750
- handle.on("change", (payload) => {
1732
+ let detachChangeListener;
1733
+ let swapping = false;
1734
+ function attachChangeListener(handle) {
1735
+ const listener = (payload) => {
1751
1736
  if (updating)
1752
1737
  return;
1753
1738
  updating = true;
@@ -1756,7 +1741,75 @@ function $crdtState(options) {
1756
1741
  } finally {
1757
1742
  updating = false;
1758
1743
  }
1744
+ if (options.resolveRedirect && !swapping) {
1745
+ maybeRebind(handle, payload.doc);
1746
+ }
1747
+ };
1748
+ handle.on("change", listener);
1749
+ detachChangeListener = () => {
1750
+ handle.off("change", listener);
1751
+ };
1752
+ }
1753
+ async function maybeRebind(fromHandle, doc) {
1754
+ if (!options.resolveRedirect)
1755
+ return;
1756
+ if (swapping)
1757
+ return;
1758
+ swapping = true;
1759
+ try {
1760
+ const next = await options.resolveRedirect(fromHandle, doc);
1761
+ if (!next)
1762
+ return;
1763
+ if (next === currentHandle)
1764
+ return;
1765
+ if (next.documentId === fromHandle.documentId)
1766
+ return;
1767
+ detachChangeListener?.();
1768
+ detachChangeListener = undefined;
1769
+ currentHandle = next;
1770
+ attachChangeListener(next);
1771
+ updating = true;
1772
+ try {
1773
+ inner.value = cloneDoc(next.doc());
1774
+ } finally {
1775
+ updating = false;
1776
+ }
1777
+ } finally {
1778
+ swapping = false;
1779
+ }
1780
+ }
1781
+ async function followInitialRedirects(start) {
1782
+ if (!options.resolveRedirect)
1783
+ return start;
1784
+ let handle = start;
1785
+ const seen = new Set([handle.documentId]);
1786
+ for (;; ) {
1787
+ const doc = handle.doc();
1788
+ if (!doc)
1789
+ break;
1790
+ const next = await options.resolveRedirect(handle, doc);
1791
+ if (!next || next === handle)
1792
+ break;
1793
+ const nextIdString = next.documentId;
1794
+ if (seen.has(nextIdString))
1795
+ break;
1796
+ seen.add(nextIdString);
1797
+ handle = next;
1798
+ await handle.whenReady();
1799
+ }
1800
+ return handle;
1801
+ }
1802
+ function applyPendingMigrations(handle) {
1803
+ if (options.schemaVersion === undefined)
1804
+ return;
1805
+ const targetVersion = options.schemaVersion;
1806
+ const migrations = options.migrations ?? {};
1807
+ handle.change((doc) => {
1808
+ runMigrations(doc, targetVersion, migrations);
1809
+ setDocVersion(doc, targetVersion);
1759
1810
  });
1811
+ }
1812
+ function bindSignalToHandle() {
1760
1813
  effect2(() => {
1761
1814
  const value = inner.value;
1762
1815
  if (updating)
@@ -1772,6 +1825,21 @@ function $crdtState(options) {
1772
1825
  updating = false;
1773
1826
  }
1774
1827
  });
1828
+ }
1829
+ const loaded = (async () => {
1830
+ const initialHandle = await options.getHandle();
1831
+ await initialHandle.whenReady();
1832
+ const handle = await followInitialRedirects(initialHandle);
1833
+ currentHandle = handle;
1834
+ applyPendingMigrations(handle);
1835
+ updating = true;
1836
+ try {
1837
+ inner.value = cloneDoc(handle.doc());
1838
+ } finally {
1839
+ updating = false;
1840
+ }
1841
+ attachChangeListener(handle);
1842
+ bindSignalToHandle();
1775
1843
  })();
1776
1844
  return {
1777
1845
  key: options.key,
@@ -1951,6 +2019,13 @@ function getDocIdResolver() {
1951
2019
  function resolveDocumentId(key) {
1952
2020
  return docIdResolver?.(key) ?? deriveDocumentId(key);
1953
2021
  }
2022
+ var redirectDetector;
2023
+ function registerRedirectDetector(detector) {
2024
+ redirectDetector = detector;
2025
+ }
2026
+ function getRedirectDetector() {
2027
+ return redirectDetector;
2028
+ }
1954
2029
  function buildHandleFactory(repo, key, initialDoc) {
1955
2030
  const documentId = resolveDocumentId(key);
1956
2031
  return async () => {
@@ -1961,23 +2036,22 @@ function buildHandleFactory(repo, key, initialDoc) {
1961
2036
  const cached = repo.handles[documentId];
1962
2037
  lazyReachedRepo++;
1963
2038
  const docIdString = documentId;
2039
+ let handle;
1964
2040
  if (cached) {
1965
2041
  await withStorageTimeout("whenReady", docIdString, cached.whenReady(["ready", "unavailable"]));
1966
2042
  if (cached.state === "ready") {
1967
2043
  exitReason = "returned-cached";
1968
- return cached;
2044
+ handle = cached;
2045
+ } else {
2046
+ handle = await loadOrSeed(repo, documentId, initialDoc, docIdString, (r) => {
2047
+ exitReason = r;
2048
+ });
1969
2049
  }
2050
+ } else {
2051
+ handle = await loadOrSeed(repo, documentId, initialDoc, docIdString, (r) => {
2052
+ exitReason = r;
2053
+ });
1970
2054
  }
1971
- const loadPromise = repo.storageSubsystem?.loadDoc(documentId);
1972
- const stored = loadPromise ? await withStorageTimeout("loadDoc", docIdString, loadPromise) : undefined;
1973
- if (stored) {
1974
- exitReason = "loaded-from-storage";
1975
- return repo.find(documentId, { allowableStates: ["ready"] });
1976
- }
1977
- const seeded = Automerge.save(Automerge.from(initialDoc));
1978
- const handle = repo.import(seeded, { docId: documentId });
1979
- handle.doneLoading();
1980
- exitReason = "seeded-and-imported";
1981
2055
  return handle;
1982
2056
  } catch (err) {
1983
2057
  errorMessage = err instanceof Error ? err.message : String(err);
@@ -1997,6 +2071,19 @@ function buildHandleFactory(repo, key, initialDoc) {
1997
2071
  }
1998
2072
  };
1999
2073
  }
2074
+ async function loadOrSeed(repo, documentId, initialDoc, docIdString, setExitReason) {
2075
+ const loadPromise = repo.storageSubsystem?.loadDoc(documentId);
2076
+ const stored = loadPromise ? await withStorageTimeout("loadDoc", docIdString, loadPromise) : undefined;
2077
+ if (stored) {
2078
+ setExitReason("loaded-from-storage");
2079
+ return repo.find(documentId, { allowableStates: ["ready"] });
2080
+ }
2081
+ const seeded = Automerge.save(Automerge.from(initialDoc));
2082
+ const handle = repo.import(seeded, { docId: documentId });
2083
+ handle.doneLoading();
2084
+ setExitReason("seeded-and-imported");
2085
+ return handle;
2086
+ }
2000
2087
  function attachLoadedRejectionSink(primitive) {
2001
2088
  primitive.loaded.catch((err) => {
2002
2089
  recordLoadedRejection(err);
@@ -2010,11 +2097,35 @@ function $meshState(key, initialValue, options = {}) {
2010
2097
  primitive: "meshState",
2011
2098
  initialValue,
2012
2099
  getHandle: buildHandleFactory(repo, key, initialValue),
2100
+ resolveRedirect: buildRedirectResolver(repo),
2013
2101
  schemaVersion: options.schemaVersion,
2014
2102
  migrations: options.migrations,
2015
2103
  access: options.access
2016
2104
  }));
2017
2105
  }
2106
+ function buildRedirectResolver(repo) {
2107
+ if (!redirectDetector) {}
2108
+ return async (_handle, doc) => {
2109
+ const detector = redirectDetector;
2110
+ if (!detector)
2111
+ return;
2112
+ let nextId;
2113
+ try {
2114
+ nextId = detector(doc);
2115
+ } catch {
2116
+ return;
2117
+ }
2118
+ if (!nextId)
2119
+ return;
2120
+ try {
2121
+ return await repo.find(nextId, {
2122
+ allowableStates: ["ready", "unavailable"]
2123
+ });
2124
+ } catch {
2125
+ return;
2126
+ }
2127
+ };
2128
+ }
2018
2129
  function $meshText(key, initialValue, options = {}) {
2019
2130
  const repo = resolveRepo(options.repo);
2020
2131
  return attachLoadedRejectionSink($crdtText(key, initialValue, {
@@ -2253,7 +2364,9 @@ function serialiseSlotView(slot) {
2253
2364
  selectedCandidatePair: slot.transport.selectedCandidatePair ? { ...slot.transport.selectedCandidatePair } : undefined
2254
2365
  } : undefined,
2255
2366
  lastSyncHandshakeAttempt: { ...slot.lastSyncHandshakeAttempt },
2256
- handles
2367
+ handles,
2368
+ createdAt: slot.createdAt,
2369
+ lastInboundAt: slot.lastInboundAt
2257
2370
  };
2258
2371
  }
2259
2372
  function emptySyncHandshakeAttempt() {
@@ -2280,6 +2393,9 @@ var DEFAULT_ICE_SERVERS = [
2280
2393
  { urls: "stun:stun.l.google.com:19302" },
2281
2394
  { urls: "stun:stun1.l.google.com:19302" }
2282
2395
  ];
2396
+ var SLOT_NEVER_CONNECTED_TIMEOUT_MS = 30000;
2397
+ var SLOT_IDLE_TIMEOUT_MS = 120000;
2398
+ var SLOT_WATCHDOG_INTERVAL_MS = 5000;
2283
2399
  function emptyHandleSyncSnapshot() {
2284
2400
  return {
2285
2401
  lastSyncMessageOutAt: undefined,
@@ -2310,6 +2426,10 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2310
2426
  lastSlotInitiationDecisions = new Map;
2311
2427
  sweepRunCount = 0;
2312
2428
  lastSweepAt;
2429
+ slotWatchdogTimer;
2430
+ slotNeverConnectedTimeoutMs;
2431
+ slotIdleTimeoutMs;
2432
+ slotWatchdogIntervalMs;
2313
2433
  get knownPeerIds() {
2314
2434
  if (this.keyringSource !== undefined) {
2315
2435
  return [...this.keyringSource().knownPeers.keys()].filter((id) => id !== this.localPeerId);
@@ -2335,6 +2455,9 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2335
2455
  this.knownPeersRefreshIntervalMs = options.knownPeersRefreshIntervalMs ?? 2000;
2336
2456
  this.syncYieldEnabled = options.syncYieldEnabled ?? true;
2337
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;
2338
2461
  this.localPeerId = options.peerId;
2339
2462
  const PC = options.RTCPeerConnection ?? globalThis.RTCPeerConnection;
2340
2463
  if (typeof PC !== "function") {
@@ -2492,9 +2615,11 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2492
2615
  this.ready = true;
2493
2616
  this.readyResolver?.();
2494
2617
  this.startKnownPeersSweep();
2618
+ this.startSlotWatchdog();
2495
2619
  }
2496
2620
  disconnect() {
2497
2621
  this.stopKnownPeersSweep();
2622
+ this.stopSlotWatchdog();
2498
2623
  for (const slot of this.slots.values()) {
2499
2624
  slot.channel?.close();
2500
2625
  slot.connection.close();
@@ -2525,6 +2650,70 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2525
2650
  clearInterval(this.knownPeersRefreshTimer);
2526
2651
  this.knownPeersRefreshTimer = undefined;
2527
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
+ }
2528
2717
  send(message) {
2529
2718
  const targetId = message.targetId;
2530
2719
  const bytes = this.serialiseMessage(message);
@@ -2660,12 +2849,23 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2660
2849
  transport: undefined,
2661
2850
  lastDataChannelError: undefined,
2662
2851
  lastSyncHandshakeAttempt: emptySyncHandshakeAttempt(),
2663
- handles: new Map
2852
+ handles: new Map,
2853
+ createdAt: performance.now(),
2854
+ lastInboundAt: undefined
2664
2855
  };
2665
2856
  this.slots.set(targetId, slot);
2666
2857
  this.wireConnection(targetId, connection);
2667
2858
  this.wireDataChannel(targetId, channel);
2668
- 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
+ });
2669
2869
  return slot;
2670
2870
  }
2671
2871
  async initiateOffer(targetId, connection) {
@@ -2703,7 +2903,9 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2703
2903
  transport: undefined,
2704
2904
  lastDataChannelError: undefined,
2705
2905
  lastSyncHandshakeAttempt: emptySyncHandshakeAttempt(),
2706
- handles: new Map
2906
+ handles: new Map,
2907
+ createdAt: performance.now(),
2908
+ lastInboundAt: undefined
2707
2909
  };
2708
2910
  this.slots.set(fromPeerId, slot);
2709
2911
  this.wireConnection(fromPeerId, connection);
@@ -2775,6 +2977,8 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2775
2977
  if (state === "connected") {
2776
2978
  this.emitPeerCandidateOnce(peerId);
2777
2979
  } else if (state === "disconnected" || state === "failed" || state === "closed") {
2980
+ if (!this.slots.has(peerId))
2981
+ return;
2778
2982
  this.slots.delete(peerId);
2779
2983
  this.emit("peer-disconnected", { peerId });
2780
2984
  }
@@ -2805,6 +3009,9 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2805
3009
  slot.pendingSends = [];
2806
3010
  };
2807
3011
  channel.onmessage = (event) => {
3012
+ const liveSlot = this.slots.get(peerId);
3013
+ if (liveSlot)
3014
+ liveSlot.lastInboundAt = performance.now();
2808
3015
  const data = event.data;
2809
3016
  if (data instanceof ArrayBuffer) {
2810
3017
  this.dispatchMessage(peerId, new Uint8Array(data));
@@ -3630,6 +3837,7 @@ export {
3630
3837
  revokePeerLocally,
3631
3838
  resolveDocumentId,
3632
3839
  resetMeshState,
3840
+ registerRedirectDetector,
3633
3841
  registerDocIdResolver,
3634
3842
  parsePairingToken,
3635
3843
  memoryKeyringStorage,
@@ -3637,6 +3845,7 @@ export {
3637
3845
  isMeshStateConfigured,
3638
3846
  isBlobRef,
3639
3847
  getStorageOpenError,
3848
+ getRedirectDetector,
3640
3849
  getMeshStateModuleId,
3641
3850
  getLazyWrappers,
3642
3851
  getLazyReachedRepo,
@@ -3694,4 +3903,4 @@ export {
3694
3903
  $meshCounter
3695
3904
  };
3696
3905
 
3697
- //# debugId=82752400E00815C464756E2164756E21
3906
+ //# debugId=9FE5660EB3793A1964756E2164756E21