@fairfox/polly 0.54.0 → 0.56.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,138 @@ 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
+ }
222
+ /** Reasons the adapter declined to construct an RTC slot for a peer
223
+ * that appeared in the signalling roster or the keyring. Recorded
224
+ * per-peer in {@link MeshWebRTCAdapter.getPeerStateSnapshot} so a
225
+ * consumer harness observing "(no slot)" can tell which gate inside
226
+ * the adapter stopped construction without having to log-correlate
227
+ * through three layers of timing. Polly issue #106 item 7.
228
+ *
229
+ * - `self`: the peerId equals the local peerId.
230
+ * - `not-in-keyring`: the live keyring (or captured Set) does not
231
+ * currently authorise this peer.
232
+ * - `not-present`: the peer is not in the signalling roster. The
233
+ * adapter only dials peers it has heard about through
234
+ * `peers-present` or `peer-joined`; keyring entries that have not
235
+ * appeared on signalling are quietly held without a slot.
236
+ * - `tie-break-other-side`: the lex-tie-break designates the remote
237
+ * peer as the initiator; we wait for their offer.
238
+ * - `slot-already-exists`: a slot exists already, possibly in any
239
+ * negotiation state.
240
+ * - `fatal-error`: an exception was thrown while attempting to build
241
+ * the slot. The accompanying {@link SlotInitiationDecision.error}
242
+ * string carries the message.
243
+ */
244
+ export type SlotInitiationRejectionReason = "self" | "not-in-keyring" | "not-present" | "tie-break-other-side" | "slot-already-exists" | "fatal-error";
245
+ /** Most-recent slot-initiation decision for a peer. Computed at
246
+ * snapshot time so the view always reflects the current state of the
247
+ * relevant gates; a `decision === "accepted"` value paired with a
248
+ * `slot === undefined` view on the same snapshot is the load-bearing
249
+ * signal for "the adapter wants to dial but isn't" — the failure
250
+ * shape polly#106 documents. */
251
+ export interface SlotInitiationDecision {
252
+ /** "accepted" means every gate in `shouldInitiateTo` would pass right
253
+ * now and a sweep tick would construct a slot. "rejected" means at
254
+ * least one gate failed; the `reason` names it. */
255
+ decision: "accepted" | "rejected";
256
+ /** Populated only on `rejected` decisions. */
257
+ reason: SlotInitiationRejectionReason | undefined;
258
+ /** If the previous sweep tick caught a synchronous throw while
259
+ * building this peer's slot, the message is preserved here for the
260
+ * next snapshot. The reason will be `fatal-error`. */
261
+ error: string | undefined;
262
+ /** `performance.now()` at the time the decision was computed. */
263
+ at: number;
264
+ }
265
+ /** Most-recent sync-handshake timeline for a peer slot. Each timestamp
266
+ * is `performance.now()` of the corresponding event the first time it
267
+ * fired on the current slot — slots that get evicted and rebuilt start
268
+ * over. The four fields together describe whether the adapter and the
269
+ * receiver downstream of it have done their part in initiating sync:
270
+ *
271
+ * - `dataChannelOpenedAt`: when the wire is ready to carry bytes.
272
+ * - `peerCandidateEmittedAt`: when polly emitted `peer-candidate`
273
+ * upward; Automerge's network subsystem hooks this event to add the
274
+ * peer to its known set and kick off the per-document sync exchange.
275
+ * If this is `undefined` long after the data channel has opened,
276
+ * polly never signalled "ready" to Automerge — that's a failure in
277
+ * the adapter.
278
+ * - `firstOutboundSendAt`: when polly first dispatched bytes through
279
+ * {@link MeshWebRTCAdapter.send} for this peer. If
280
+ * `peerCandidateEmittedAt` is set but this is still `undefined`, the
281
+ * wrapper above polly (Automerge's NetworkSubsystem, MeshNetworkAdapter)
282
+ * never asked the adapter to send anything; that's a failure
283
+ * upstream of polly.
284
+ * - `firstInboundMessageAt`: when polly first emitted a `message` event
285
+ * for this peer. If `peerCandidateEmittedAt` is set on the remote and
286
+ * `firstOutboundSendAt` is set on the remote but this is `undefined`
287
+ * locally, bytes were sent across the wire but never decoded — that
288
+ * points at the crypto envelope or the wire fragmenter.
289
+ *
290
+ * Polly issue #106 item 7. */
291
+ export interface SyncHandshakeAttemptSnapshot {
292
+ dataChannelOpenedAt: number | undefined;
293
+ peerCandidateEmittedAt: number | undefined;
294
+ firstOutboundSendAt: number | undefined;
295
+ firstInboundMessageAt: number | undefined;
296
+ }
297
+ /** Sweep-loop observability for the periodic dial re-evaluation. The
298
+ * sweep is what catches peers that were not in the keyring at the time
299
+ * of their `peer-joined` notification (polly#103) AND peers whose
300
+ * previous slot failed and got evicted (polly#106). Exposing its tick
301
+ * counter lets a consumer distinguish "sweep is running but
302
+ * `shouldInitiateTo` rejected the peer" from "sweep is broken and
303
+ * never fires" without instrumenting polly internals. */
304
+ export interface SweepSnapshot {
305
+ /** True iff the sweep timer is currently scheduled — false on the
306
+ * captured-set fallback path (no keyringSource) or when the interval
307
+ * is configured to 0. */
308
+ enabled: boolean;
309
+ /** Configured interval in milliseconds. 0 means disabled. */
310
+ intervalMs: number;
311
+ /** How many times the sweep callback has fired since `connect()`. */
312
+ runCount: number;
313
+ /** `performance.now()` at the last tick. `undefined` until the
314
+ * first tick fires. */
315
+ lastRunAt: number | undefined;
316
+ }
164
317
  /** Per-peer view of an in-flight initial sync. Populated by
165
318
  * {@link MeshWebRTCAdapter.handleSyncFragment} as fragments of a
166
319
  * single reassembly arrive, and reset to `undefined` once the
@@ -192,6 +345,8 @@ export interface InFlightSyncSnapshot {
192
345
  export declare class MeshWebRTCAdapter extends NetworkAdapter {
193
346
  readonly signaling: MeshSignalingClient;
194
347
  readonly iceServers: RTCIceServer[];
348
+ readonly iceTransportPolicy: RTCIceTransportPolicy | undefined;
349
+ private readonly iceRelayEnforcement;
195
350
  readonly dataChannelLabel: string;
196
351
  /** Peers this adapter is willing to dial. Mutable so callers that pair
197
352
  * a new device after construction (e.g. a CLI `add-device` process whose
@@ -241,6 +396,21 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
241
396
  private readonly slots;
242
397
  private ready;
243
398
  private readyResolver;
399
+ /** Sticky cache of the most recent {@link SlotInitiationDecision}
400
+ * per peer. Updated on every `shouldInitiateTo` call and on every
401
+ * caught throw inside the sweep loop, so a snapshot taken at any
402
+ * moment can answer "why is there no slot for this peer right
403
+ * now?". Polly issue #106 item 7. */
404
+ private readonly lastSlotInitiationDecisions;
405
+ /** Tick count of the periodic sweep — incremented inside the
406
+ * `setInterval` callback, exposed via {@link getPeerStateSnapshot}.
407
+ * Lets a consumer rule out "sweep is dead" before chasing the
408
+ * shouldInitiateTo gates. */
409
+ private sweepRunCount;
410
+ /** `performance.now()` at the last sweep tick. Paired with
411
+ * {@link sweepRunCount} so a stalled sweep is visible at a glance
412
+ * via the snapshot's `sweep` block. */
413
+ private lastSweepAt;
244
414
  /** The peers this adapter will dial. Backward-compatible read accessor
245
415
  * for callers that previously iterated the `knownPeerIds` array. With
246
416
  * a {@link MeshWebRTCAdapterOptions.keyringSource} configured, the
@@ -288,10 +458,13 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
288
458
  localPeerId: string;
289
459
  knownPeerIds: string[];
290
460
  presentPeerIds: string[];
461
+ sweep: SweepSnapshot;
291
462
  peers: Array<{
292
463
  peerId: string;
293
464
  knownInKeyring: boolean;
294
465
  presentInSignalling: boolean;
466
+ slotInitiationRejectionReason: SlotInitiationRejectionReason | undefined;
467
+ slotInitiationDecision: SlotInitiationDecision;
295
468
  slot: undefined | {
296
469
  signalingState: string;
297
470
  iceConnectionState: string;
@@ -300,6 +473,8 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
300
473
  pendingSendCount: number;
301
474
  pendingRemoteIceCount: number;
302
475
  inFlightSync: InFlightSyncSnapshot | undefined;
476
+ transport: TransportSnapshot | undefined;
477
+ lastSyncHandshakeAttempt: SyncHandshakeAttemptSnapshot;
303
478
  };
304
479
  }>;
305
480
  };
@@ -317,6 +492,17 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
317
492
  * every listed peer, so a device joining into an established lobby
318
493
  * dials every knownPeer it is meant to initiate to in one pass. */
319
494
  handlePeersPresent(peerIds: string[]): void;
495
+ /** Construct an initiating slot inside a per-peer try/catch and
496
+ * record any throw as a `fatal-error` rejection on the per-peer
497
+ * decision map so the snapshot surface names it. Every dial entry
498
+ * point ({@link handlePeerJoined}, {@link handlePeersPresent},
499
+ * {@link addKnownPeer}, {@link refreshKnownPeers}) routes through
500
+ * here so a single peer's broken construction can never take down a
501
+ * batch of peers — pre-#106 a thrown `new RTCPeerConnection()`
502
+ * inside `handlePeersPresent` would skip every later peer in the
503
+ * same batch with no observable trace because the signalling
504
+ * client's frame dispatch swallowed the rejection. */
505
+ private tryCreateInitiatingSlot;
320
506
  /** Handle the signalling server's `peer-left` notification: a
321
507
  * previously joined peer has closed its socket. Evict any slot we
322
508
  * hold for that peer so a subsequent `peer-joined` from the same
@@ -345,9 +531,41 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
345
531
  * dial the ones the keyring authorises that we do not already have
346
532
  * a slot for. The periodic sweep started in {@link connect} calls
347
533
  * this; consumers can call it manually to skip the wait after they
348
- * apply a fresh pairing token. Idempotent. */
534
+ * apply a fresh pairing token. Idempotent.
535
+ *
536
+ * A throw from {@link createInitiatingSlot} for one peer must not
537
+ * prevent the sweep from continuing to the next one — pre-#106 a
538
+ * synchronous throw inside `new RTCPeerConnection()` (a real risk
539
+ * once the page has built dozens of connections and Chrome's
540
+ * per-page cap is in play) skipped every later peer in the same
541
+ * sweep, with no observable trace because `setInterval` swallows
542
+ * the rejection silently. {@link tryCreateInitiatingSlot} caches
543
+ * the error onto the snapshot's slotInitiationRejectionReason as
544
+ * `fatal-error` so the failing peer is named instead of disguised
545
+ * as "(no slot)". */
349
546
  refreshKnownPeers(): void;
350
547
  private shouldInitiateTo;
548
+ /** Pure-function form of the gate cascade behind {@link shouldInitiateTo}.
549
+ * Returns `undefined` when every gate passes (the slot would be
550
+ * built); otherwise returns the named reason the dial was declined.
551
+ * Pulling the gates out of the boolean wrapper lets the snapshot
552
+ * surface name *which* gate stopped construction without the caller
553
+ * having to re-implement the check. Polly issue #106 item 7.
554
+ *
555
+ * The "not-present" gate is checked here even though the inbound
556
+ * call sites (`handlePeerJoined`, `handlePeersPresent`,
557
+ * `refreshKnownPeers`, `addKnownPeer`) only invoke `shouldInitiateTo`
558
+ * for peers in the signalling roster — so on those paths the gate
559
+ * never fires. It's load-bearing on the snapshot path, where the
560
+ * reason is computed for every peer the caller might ask about
561
+ * (including keyring entries that aren't currently signalling). */
562
+ private evaluateInitiation;
563
+ /** Compute the latest initiation decision for a peer at snapshot
564
+ * time. Prefers the cached decision when a sweep tick fixed the
565
+ * outcome to `fatal-error` (a thrown construction is sticky until
566
+ * the next successful sweep clears it); otherwise re-evaluates the
567
+ * gates against current state. Pure read; never mutates the map. */
568
+ private snapshotInitiationDecision;
351
569
  whenReady(): Promise<void>;
352
570
  /**
353
571
  * Start the adapter. Marks the adapter ready so Automerge's
@@ -367,7 +585,15 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
367
585
  * keyring after their `peer-joined` notification has already fired.
368
586
  * No-op when no keyringSource was supplied — the captured-set
369
587
  * fallback has no live source to re-read, so the sweep would be
370
- * useless. No-op when the interval is configured to 0. */
588
+ * useless. No-op when the interval is configured to 0.
589
+ *
590
+ * Each tick increments {@link sweepRunCount} and stamps
591
+ * {@link lastSweepAt} *before* dispatching to
592
+ * {@link refreshKnownPeers} so a snapshot can distinguish "sweep is
593
+ * running but every peer is rejected" from "sweep is dead". An
594
+ * outer try/catch keeps the timer alive even if the per-peer
595
+ * try/catch inside `refreshKnownPeers` somehow leaks. Polly issue
596
+ * #106 item 7. */
371
597
  private startKnownPeersSweep;
372
598
  private stopKnownPeersSweep;
373
599
  /**
@@ -411,6 +637,44 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
411
637
  * method.
412
638
  */
413
639
  handleSignal(fromPeerId: string, rawPayload: unknown): void;
640
+ /** Assemble the {@link RTCConfiguration} every new
641
+ * {@link RTCPeerConnection} is built with. Centralised so every slot
642
+ * (initiator and answerer) honours the same iceTransportPolicy. */
643
+ private buildRtcConfiguration;
644
+ /** Decide whether a local ICE candidate should be relayed through
645
+ * the signalling channel to the remote peer. Chrome's iceTransport
646
+ * Policy = "relay" implementation filters non-relay candidates at
647
+ * the source so the remote peer never sees them; werift's only
648
+ * filters its own outgoing connectivity checks and still emits host
649
+ * and srflx candidates upstream, so a remote peer with policy "all"
650
+ * can pair against them — making relay-only enforcement leaky in
651
+ * the mixed-implementation case the polly#105 falsification harness
652
+ * exposes. Mirroring Chrome's filter at this layer gives a uniform
653
+ * contract on every RTCPeerConnection implementation polly supports.
654
+ *
655
+ * The candidate `type` field is the SDP-spec form; we additionally
656
+ * parse it out of the legacy `candidate` string for implementations
657
+ * that don't surface the typed field directly (werift exposes the
658
+ * SDP string only). */
659
+ private isRelayCandidateInit;
660
+ private shouldSendCandidate;
661
+ /** Strip non-relay `a=candidate:` lines from an SDP when
662
+ * iceTransportPolicy is `"relay"`. Some RTCPeerConnection
663
+ * implementations (werift, notably) embed every gathered candidate
664
+ * in the SDP regardless of policy, and a remote peer parsing those
665
+ * via `setRemoteDescription` will pair against them — bypassing the
666
+ * relay-only contract. Chrome already filters in-SDP candidates by
667
+ * policy, so this is only load-bearing for werift consumers, but
668
+ * the SDP rewrite is implementation-agnostic and idempotent. */
669
+ private filterSdpCandidatesByPolicy;
670
+ /** Apply {@link filterSdpCandidatesByPolicy} to an SDP
671
+ * description ahead of `setLocalDescription` (filter outgoing) or
672
+ * `setRemoteDescription` (filter incoming). The defensive
673
+ * receive-side filter exists because we cannot trust the remote
674
+ * peer's adapter to have stripped its non-relay candidates first —
675
+ * polly might be talking to a pre-#105 polly, or to a non-polly
676
+ * peer whose policy enforcement is different. */
677
+ private applySdpPolicyFilter;
414
678
  private createInitiatingSlot;
415
679
  private initiateOffer;
416
680
  private handleOffer;
@@ -422,7 +686,35 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
422
686
  * them — a single bad candidate must not stall the connection. */
423
687
  private flushPendingRemoteIce;
424
688
  private wireConnection;
689
+ /** Emit `peer-candidate` upward exactly once per slot lifetime,
690
+ * stamping the slot's `lastSyncHandshakeAttempt.peerCandidateEmittedAt`
691
+ * with the moment of emission. The "once" semantics protect Automerge's
692
+ * NetworkSubsystem from a double-add when both the
693
+ * `connectionstatechange = connected` event AND the
694
+ * `dataChannel.onopen` event fire on the same slot — under werift
695
+ * the former is sometimes flaky, under Chrome the latter sometimes
696
+ * fires first; pre-#106 only the connection-state path emitted, so a
697
+ * werift slot whose data channel opened cleanly but whose connection
698
+ * state never advanced past `connecting` would never signal "ready"
699
+ * upward. Polly issue #106 Failure B item — closing one named
700
+ * mechanism for "data channel open, sync never started". */
701
+ private emitPeerCandidateOnce;
425
702
  private wireDataChannel;
703
+ /** Refresh the per-peer {@link TransportSnapshot} for one peer by
704
+ * pulling {@link RTCPeerConnection.getStats} and distilling the
705
+ * result. Cheap-ish but not free — getStats walks the underlying
706
+ * stats graph — so this is opt-in: callers (typically a debugging
707
+ * harness or a periodic observability loop) invoke it explicitly
708
+ * and the result lives on the slot for the next
709
+ * {@link getPeerStateSnapshot}. Returns the refreshed snapshot, or
710
+ * `undefined` if there is no slot for the peer or stats are
711
+ * unavailable. Polly issue #105 item 7. */
712
+ refreshTransportStats(peerId: string): Promise<TransportSnapshot | undefined>;
713
+ /** Refresh transport stats for every active peer slot in one shot.
714
+ * Returns a map keyed by peerId so a caller can render the result
715
+ * without re-walking {@link getPeerStateSnapshot}. The underlying
716
+ * `getStats` calls run concurrently. */
717
+ refreshAllTransportStats(): Promise<Map<string, TransportSnapshot>>;
426
718
  private dispatchMessage;
427
719
  /** Hand a deserialised Automerge message off to whoever is listening
428
720
  * on this adapter's `"message"` event. When
@@ -439,6 +731,11 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
439
731
  * `inFlightSync` reassembly state worth bookkeeping. Small
440
732
  * single-message dispatches yield but don't touch inFlightSync. */
441
733
  private scheduleEmitMessage;
734
+ /** Stamp the slot's `firstInboundMessageAt` the first time a
735
+ * dispatched (non-fragment, non-blob) message lands for a peer. Pure
736
+ * observability for {@link SyncHandshakeAttemptSnapshot}; does not
737
+ * affect dispatch. */
738
+ private stampFirstInboundMessage;
442
739
  private finishInFlightSyncApply;
443
740
  private emitSyncProgress;
444
741
  private handleSyncFragment;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fairfox/polly",
3
- "version": "0.54.0",
3
+ "version": "0.56.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",