@fairfox/polly 0.54.0 → 0.55.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.
@@ -75,6 +75,21 @@ export interface CreateMeshClientOptions {
75
75
  * the credential window closes.
76
76
  */
77
77
  iceCredentialResolver?: () => Promise<RTCIceServer[]>;
78
+ /** Forward of {@link MeshWebRTCAdapterOptions.iceTransportPolicy}.
79
+ * Set to `"relay"` to force every candidate pair through TURN; the
80
+ * default leaves the underlying {@link RTCPeerConnection}
81
+ * implementation's default in place. The
82
+ * `examples/mesh-large-initial-sync-turn` harness uses `"relay"` to
83
+ * exercise the polly#105 real-transport contract. */
84
+ iceTransportPolicy?: MeshWebRTCAdapterOptions["iceTransportPolicy"];
85
+ /** Forward of {@link MeshWebRTCAdapterOptions.iceRelayEnforcement}.
86
+ * Defaults to `true`. Set to `false` to bypass the polly#105
87
+ * relay-only enforcement layer; the
88
+ * `examples/mesh-large-initial-sync-turn` falsification path
89
+ * (`POLLY_105_DISABLE_TURN_FIX=1`) does this to reproduce the
90
+ * pre-#105 candidate-leak shape. Production callers should leave
91
+ * this at the default. */
92
+ iceRelayEnforcement?: MeshWebRTCAdapterOptions["iceRelayEnforcement"];
78
93
  dataChannelLabel?: string;
79
94
  /** How often the mesh client re-evaluates whether to dial peers
80
95
  * already present in the signalling roster against the live
@@ -186,6 +201,14 @@ export interface MeshClient {
186
201
  * harness can answer "is the mesh layer in a known good state"
187
202
  * without instrumenting polly internals. Polly issue #103 item 7. */
188
203
  getPeerStateSnapshot(): ReturnType<MeshWebRTCAdapter["getPeerStateSnapshot"]>;
204
+ /** Refresh every active peer slot's transport-level summary —
205
+ * selected ICE candidate pair, SCTP retransmission counters, last
206
+ * data-channel error — and populate it into the next
207
+ * {@link getPeerStateSnapshot}. Walks {@link RTCPeerConnection.getStats}
208
+ * once per peer, so it isn't free; consumers that want continuous
209
+ * visibility should call this on a polling cadence the cost can
210
+ * absorb. Polly issue #105 item 7. */
211
+ refreshTransportStats(): Promise<void>;
189
212
  /** Close the signalling WebSocket, tear down every RTCPeerConnection,
190
213
  * and shut the Repo cleanly. Idempotent. */
191
214
  close(): Promise<void>;
@@ -102,6 +102,27 @@ export interface MeshWebRTCAdapterOptions {
102
102
  knownPeersRefreshIntervalMs?: number;
103
103
  /** Optional ICE server list override. Defaults to {@link DEFAULT_ICE_SERVERS}. */
104
104
  iceServers?: RTCIceServer[];
105
+ /** Optional ICE transport policy. Defaults to the
106
+ * {@link RTCPeerConnection} implementation's own default (`"all"` in
107
+ * Chrome, Firefox, and werift). Set to `"relay"` to force every
108
+ * candidate pair through a TURN relay — the shape the polly#105
109
+ * falsification harness needs to exercise the real-transport contract
110
+ * the polly#104 in-process throttle could not reach. */
111
+ iceTransportPolicy?: RTCIceTransportPolicy;
112
+ /** When `true` (the default), polly enforces
113
+ * {@link iceTransportPolicy} `"relay"` on its side of the signalling
114
+ * channel by filtering non-relay ICE candidates out of both the SDP
115
+ * description it forwards and the trickle ICE stream it emits. This
116
+ * closes the gap exposed by polly#105: Chrome's implementation
117
+ * already filters at the source, but werift only filters its own
118
+ * outgoing connectivity checks and still advertises host and srflx
119
+ * candidates upstream — so a relay-only peer on werift will, against
120
+ * a peer with the default policy, still pair through a non-relay
121
+ * candidate (or against a host-derived peer-reflexive remote).
122
+ * Setting this option to `false` reverts the enforcement and is the
123
+ * shape used by the `POLLY_105_DISABLE_TURN_FIX=1` falsification
124
+ * path. Production callers should leave this at the default. */
125
+ iceRelayEnforcement?: boolean;
105
126
  /** Optional data channel label. Defaults to "polly-mesh". Applications
106
127
  * that share a signalling server between multiple meshes may want
107
128
  * distinct labels per mesh. */
@@ -161,6 +182,43 @@ export interface SyncProgressEvent {
161
182
  /** `performance.now()` at event emission. */
162
183
  at: number;
163
184
  }
185
+ /** Last-seen transport-level summary for a peer slot. Populated
186
+ * lazily by {@link MeshWebRTCAdapter.refreshTransportStats}, which
187
+ * the consumer calls (typically from a polling loop in a debugging
188
+ * harness) so the cost of {@link RTCPeerConnection.getStats} is
189
+ * incurred only on demand. Polly issue #105 item 7 — exposes the
190
+ * dimension of the transport that {@link InFlightSyncSnapshot}
191
+ * doesn't reach: the negotiated ICE candidate pair, the SCTP
192
+ * retransmission counters, and the last data-channel-level error if
193
+ * one has been observed.
194
+ *
195
+ * Field names mirror the W3C stats spec (`selectedCandidatePairId`,
196
+ * `localCandidateType`, etc.) so consumers can correlate with the
197
+ * raw `RTCStatsReport`. werift exposes a stats shape close to the
198
+ * spec; Chrome exposes the spec itself; this view is implementation-
199
+ * agnostic. */
200
+ export interface TransportSnapshot {
201
+ /** ICE-level summary of the pair currently carrying data. */
202
+ selectedCandidatePair: {
203
+ localCandidateType: string;
204
+ remoteCandidateType: string;
205
+ state: string;
206
+ nominated: boolean;
207
+ bytesSent: number;
208
+ bytesReceived: number;
209
+ } | undefined;
210
+ /** SCTP / data-channel retransmission counters. Some implementations
211
+ * surface these on the transport stat, others on the data-channel
212
+ * stat — we expose the values we found, leaving the field undefined
213
+ * when the underlying impl doesn't surface them. */
214
+ retransmittedPacketsSent: number | undefined;
215
+ retransmittedBytesSent: number | undefined;
216
+ /** Most-recent data-channel error message, if `error` ever fired on
217
+ * the channel for this peer. Cleared only when the slot is replaced. */
218
+ lastDataChannelError: string | undefined;
219
+ /** `performance.now()` at the time the stats were last refreshed. */
220
+ at: number;
221
+ }
164
222
  /** Per-peer view of an in-flight initial sync. Populated by
165
223
  * {@link MeshWebRTCAdapter.handleSyncFragment} as fragments of a
166
224
  * single reassembly arrive, and reset to `undefined` once the
@@ -192,6 +250,8 @@ export interface InFlightSyncSnapshot {
192
250
  export declare class MeshWebRTCAdapter extends NetworkAdapter {
193
251
  readonly signaling: MeshSignalingClient;
194
252
  readonly iceServers: RTCIceServer[];
253
+ readonly iceTransportPolicy: RTCIceTransportPolicy | undefined;
254
+ private readonly iceRelayEnforcement;
195
255
  readonly dataChannelLabel: string;
196
256
  /** Peers this adapter is willing to dial. Mutable so callers that pair
197
257
  * a new device after construction (e.g. a CLI `add-device` process whose
@@ -300,6 +360,7 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
300
360
  pendingSendCount: number;
301
361
  pendingRemoteIceCount: number;
302
362
  inFlightSync: InFlightSyncSnapshot | undefined;
363
+ transport: TransportSnapshot | undefined;
303
364
  };
304
365
  }>;
305
366
  };
@@ -411,6 +472,44 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
411
472
  * method.
412
473
  */
413
474
  handleSignal(fromPeerId: string, rawPayload: unknown): void;
475
+ /** Assemble the {@link RTCConfiguration} every new
476
+ * {@link RTCPeerConnection} is built with. Centralised so every slot
477
+ * (initiator and answerer) honours the same iceTransportPolicy. */
478
+ private buildRtcConfiguration;
479
+ /** Decide whether a local ICE candidate should be relayed through
480
+ * the signalling channel to the remote peer. Chrome's iceTransport
481
+ * Policy = "relay" implementation filters non-relay candidates at
482
+ * the source so the remote peer never sees them; werift's only
483
+ * filters its own outgoing connectivity checks and still emits host
484
+ * and srflx candidates upstream, so a remote peer with policy "all"
485
+ * can pair against them — making relay-only enforcement leaky in
486
+ * the mixed-implementation case the polly#105 falsification harness
487
+ * exposes. Mirroring Chrome's filter at this layer gives a uniform
488
+ * contract on every RTCPeerConnection implementation polly supports.
489
+ *
490
+ * The candidate `type` field is the SDP-spec form; we additionally
491
+ * parse it out of the legacy `candidate` string for implementations
492
+ * that don't surface the typed field directly (werift exposes the
493
+ * SDP string only). */
494
+ private isRelayCandidateInit;
495
+ private shouldSendCandidate;
496
+ /** Strip non-relay `a=candidate:` lines from an SDP when
497
+ * iceTransportPolicy is `"relay"`. Some RTCPeerConnection
498
+ * implementations (werift, notably) embed every gathered candidate
499
+ * in the SDP regardless of policy, and a remote peer parsing those
500
+ * via `setRemoteDescription` will pair against them — bypassing the
501
+ * relay-only contract. Chrome already filters in-SDP candidates by
502
+ * policy, so this is only load-bearing for werift consumers, but
503
+ * the SDP rewrite is implementation-agnostic and idempotent. */
504
+ private filterSdpCandidatesByPolicy;
505
+ /** Apply {@link filterSdpCandidatesByPolicy} to an SDP
506
+ * description ahead of `setLocalDescription` (filter outgoing) or
507
+ * `setRemoteDescription` (filter incoming). The defensive
508
+ * receive-side filter exists because we cannot trust the remote
509
+ * peer's adapter to have stripped its non-relay candidates first —
510
+ * polly might be talking to a pre-#105 polly, or to a non-polly
511
+ * peer whose policy enforcement is different. */
512
+ private applySdpPolicyFilter;
414
513
  private createInitiatingSlot;
415
514
  private initiateOffer;
416
515
  private handleOffer;
@@ -423,6 +522,21 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
423
522
  private flushPendingRemoteIce;
424
523
  private wireConnection;
425
524
  private wireDataChannel;
525
+ /** Refresh the per-peer {@link TransportSnapshot} for one peer by
526
+ * pulling {@link RTCPeerConnection.getStats} and distilling the
527
+ * result. Cheap-ish but not free — getStats walks the underlying
528
+ * stats graph — so this is opt-in: callers (typically a debugging
529
+ * harness or a periodic observability loop) invoke it explicitly
530
+ * and the result lives on the slot for the next
531
+ * {@link getPeerStateSnapshot}. Returns the refreshed snapshot, or
532
+ * `undefined` if there is no slot for the peer or stats are
533
+ * unavailable. Polly issue #105 item 7. */
534
+ refreshTransportStats(peerId: string): Promise<TransportSnapshot | undefined>;
535
+ /** Refresh transport stats for every active peer slot in one shot.
536
+ * Returns a map keyed by peerId so a caller can render the result
537
+ * without re-walking {@link getPeerStateSnapshot}. The underlying
538
+ * `getStats` calls run concurrently. */
539
+ refreshAllTransportStats(): Promise<Map<string, TransportSnapshot>>;
426
540
  private dispatchMessage;
427
541
  /** Hand a deserialised Automerge message off to whoever is listening
428
542
  * on this adapter's `"message"` event. When
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fairfox/polly",
3
- "version": "0.54.0",
3
+ "version": "0.55.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",