@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.
@@ -51,6 +51,30 @@ import type { MeshSignalingClient } from "./mesh-signaling-client";
51
51
  * need TURN fallback for peers behind symmetric NATs should replace this
52
52
  * with their own ICE server list. */
53
53
  export declare const DEFAULT_ICE_SERVERS: RTCIceServer[];
54
+ /** Polly issue #109: how long a slot can sit at `connectionState` in
55
+ * `new` or `connecting` before the watchdog tears it down so the
56
+ * recovery sweep can re-attempt. Healthy ICE completes in single-digit
57
+ * seconds; 30s is well past anything legitimate and short enough that
58
+ * a silent `createOffer`/`setLocalDescription` rejection no longer
59
+ * leaves an unrecoverable wedged slot. */
60
+ export declare const SLOT_NEVER_CONNECTED_TIMEOUT_MS = 30000;
61
+ /** Polly issue #110: how long a slot whose data channel is open and
62
+ * whose `connectionState` is `connected` can have no inbound bytes
63
+ * before the watchdog assumes the remote process is dead and tears
64
+ * the slot down. The ICE keepalive timer can take minutes to notice a
65
+ * remote SIGKILL or network partition; an application-layer
66
+ * liveness check that runs an order of magnitude faster keeps
67
+ * paired devices from sending sync traffic into the void after the
68
+ * remote daemon restarts. 120s is conservative — Automerge's idle
69
+ * cadence is well below this, so a healthy slot never crosses the
70
+ * threshold. */
71
+ export declare const SLOT_IDLE_TIMEOUT_MS = 120000;
72
+ /** Polly issue #109/#110: how often the watchdog evaluates teardown
73
+ * decisions across every slot. 5s is well below either threshold so
74
+ * teardown happens promptly after a deadline lapses without
75
+ * dominating runtime cost (one `connectionState` read per slot per
76
+ * tick). */
77
+ export declare const SLOT_WATCHDOG_INTERVAL_MS = 5000;
54
78
  /** Options for constructing a {@link MeshWebRTCAdapter}. */
55
79
  export interface MeshWebRTCAdapterOptions {
56
80
  /** The signalling client the adapter uses to exchange SDP and ICE
@@ -154,6 +178,22 @@ export interface MeshWebRTCAdapterOptions {
154
178
  * `POLLY_104_DISABLE_FIX=1` falsification path. Production callers
155
179
  * should leave this at the default. */
156
180
  syncFragmentChunkSizeOverride?: number;
181
+ /** How long a slot can sit at `connectionState` in `new` or
182
+ * `connecting` before the watchdog tears it down. Defaults to
183
+ * {@link SLOT_NEVER_CONNECTED_TIMEOUT_MS}. Set to 0 to disable the
184
+ * gate. Polly issue #109. */
185
+ slotNeverConnectedTimeoutMs?: number;
186
+ /** How long a connected slot can have no inbound bytes before the
187
+ * watchdog tears it down as idle. Defaults to
188
+ * {@link SLOT_IDLE_TIMEOUT_MS}. Set to 0 to disable the gate.
189
+ * Polly issue #110. */
190
+ slotIdleTimeoutMs?: number;
191
+ /** How often the slot watchdog evaluates teardown decisions.
192
+ * Defaults to {@link SLOT_WATCHDOG_INTERVAL_MS}; tests override to
193
+ * tens of milliseconds. Set to 0 to disable the watchdog entirely
194
+ * (the pre-#109/#110 behaviour, kept only for migration and the
195
+ * falsification path). */
196
+ slotWatchdogIntervalMs?: number;
157
197
  }
158
198
  /** Payload of the polly-specific `"sync-progress"` event emitted by
159
199
  * {@link MeshWebRTCAdapter}. Consumers can subscribe via the adapter's
@@ -239,7 +279,10 @@ export interface TransportSnapshot {
239
279
  * negotiation state.
240
280
  * - `fatal-error`: an exception was thrown while attempting to build
241
281
  * the slot. The accompanying {@link SlotInitiationDecision.error}
242
- * string carries the message.
282
+ * string carries the message. This is also stamped when the slot
283
+ * watchdog tears a wedged slot down (polly#109's silent throw, or
284
+ * polly#110's idle-but-still-`connected` post-mortem), so the next
285
+ * sweep tick finds no slot and retries.
243
286
  */
244
287
  export type SlotInitiationRejectionReason = "self" | "not-in-keyring" | "not-present" | "tie-break-other-side" | "slot-already-exists" | "fatal-error";
245
288
  /** Most-recent slot-initiation decision for a peer. Computed at
@@ -478,6 +521,14 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
478
521
  * {@link sweepRunCount} so a stalled sweep is visible at a glance
479
522
  * via the snapshot's `sweep` block. */
480
523
  private lastSweepAt;
524
+ /** Watchdog interval (polly#109 + polly#110). Tears down slots that
525
+ * never reach `connected`, or that look connected but have not
526
+ * received bytes for {@link slotIdleTimeoutMs}, so the recovery
527
+ * sweep can re-attempt. Cleared in {@link disconnect}. */
528
+ private slotWatchdogTimer;
529
+ private readonly slotNeverConnectedTimeoutMs;
530
+ private readonly slotIdleTimeoutMs;
531
+ private readonly slotWatchdogIntervalMs;
481
532
  /** The peers this adapter will dial. Backward-compatible read accessor
482
533
  * for callers that previously iterated the `knownPeerIds` array. With
483
534
  * a {@link MeshWebRTCAdapterOptions.keyringSource} configured, the
@@ -543,6 +594,8 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
543
594
  transport: TransportSnapshot | undefined;
544
595
  lastSyncHandshakeAttempt: SyncHandshakeAttemptSnapshot;
545
596
  handles: Record<string, HandleSyncSnapshot>;
597
+ createdAt: number;
598
+ lastInboundAt: number | undefined;
546
599
  };
547
600
  }>;
548
601
  };
@@ -664,6 +717,41 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
664
717
  * #106 item 7. */
665
718
  private startKnownPeersSweep;
666
719
  private stopKnownPeersSweep;
720
+ /** Close a slot's data channel and connection, remove it from the
721
+ * map, and emit `peer-disconnected` upward so Automerge stops
722
+ * routing through the dead pair. Used by both the polly#109
723
+ * `.catch` in {@link createInitiatingSlot} and the polly#109/#110
724
+ * watchdog in {@link sweepWedgedSlots}. */
725
+ private tearDownWedgedSlot;
726
+ /** Start the slot watchdog (polly#109 + polly#110). Walks every
727
+ * active slot every {@link slotWatchdogIntervalMs} and tears down
728
+ * the two named wedge shapes:
729
+ *
730
+ * - `connectionState` in `new`/`connecting` for longer than
731
+ * {@link slotNeverConnectedTimeoutMs} (polly#109: silent
732
+ * `createOffer`/`setLocalDescription` rejection, or a network
733
+ * condition under which ICE never gathers).
734
+ *
735
+ * - `connectionState === "connected"` AND data channel `open` AND
736
+ * no inbound bytes for {@link slotIdleTimeoutMs} (polly#110:
737
+ * remote process killed without OS-layer FIN — ICE keepalives
738
+ * take many minutes to fail, the slot sends sync traffic into
739
+ * the void until then).
740
+ *
741
+ * Each teardown stamps `fatal-error` on the per-peer decision so
742
+ * the named gate is visible on the next snapshot, then emits
743
+ * `peer-disconnected` so Automerge stops routing through the dead
744
+ * slot. The next sweep tick re-evaluates and the recovery path
745
+ * creates a fresh slot. No-op when configured to 0. */
746
+ private startSlotWatchdog;
747
+ private stopSlotWatchdog;
748
+ private sweepWedgedSlots;
749
+ /** Decide whether a slot is wedged at the moment of inspection,
750
+ * returning the human-readable diagnosis when it is. Pulled out of
751
+ * {@link sweepWedgedSlots} so the per-slot decision stays under
752
+ * biome's cognitive-complexity ceiling and so a future test can
753
+ * exercise the gates directly. */
754
+ private classifyWedgedSlot;
667
755
  /**
668
756
  * Send a sync message to a specific remote peer. If no RTCPeerConnection
669
757
  * exists yet, the adapter initiates one by producing an SDP offer and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fairfox/polly",
3
- "version": "0.63.0",
3
+ "version": "0.65.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Multi-execution-context framework with reactive state and cross-context messaging for Chrome extensions, PWAs, and worker-based applications",