@fairfox/polly 0.55.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.
- package/dist/src/mesh.d.ts +1 -1
- package/dist/src/mesh.js +110 -17
- package/dist/src/mesh.js.map +5 -5
- package/dist/src/shared/lib/mesh-webrtc-adapter.d.ts +185 -2
- package/package.json +1 -1
|
@@ -219,6 +219,101 @@ export interface TransportSnapshot {
|
|
|
219
219
|
/** `performance.now()` at the time the stats were last refreshed. */
|
|
220
220
|
at: number;
|
|
221
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
|
+
}
|
|
222
317
|
/** Per-peer view of an in-flight initial sync. Populated by
|
|
223
318
|
* {@link MeshWebRTCAdapter.handleSyncFragment} as fragments of a
|
|
224
319
|
* single reassembly arrive, and reset to `undefined` once the
|
|
@@ -301,6 +396,21 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
|
|
|
301
396
|
private readonly slots;
|
|
302
397
|
private ready;
|
|
303
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;
|
|
304
414
|
/** The peers this adapter will dial. Backward-compatible read accessor
|
|
305
415
|
* for callers that previously iterated the `knownPeerIds` array. With
|
|
306
416
|
* a {@link MeshWebRTCAdapterOptions.keyringSource} configured, the
|
|
@@ -348,10 +458,13 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
|
|
|
348
458
|
localPeerId: string;
|
|
349
459
|
knownPeerIds: string[];
|
|
350
460
|
presentPeerIds: string[];
|
|
461
|
+
sweep: SweepSnapshot;
|
|
351
462
|
peers: Array<{
|
|
352
463
|
peerId: string;
|
|
353
464
|
knownInKeyring: boolean;
|
|
354
465
|
presentInSignalling: boolean;
|
|
466
|
+
slotInitiationRejectionReason: SlotInitiationRejectionReason | undefined;
|
|
467
|
+
slotInitiationDecision: SlotInitiationDecision;
|
|
355
468
|
slot: undefined | {
|
|
356
469
|
signalingState: string;
|
|
357
470
|
iceConnectionState: string;
|
|
@@ -361,6 +474,7 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
|
|
|
361
474
|
pendingRemoteIceCount: number;
|
|
362
475
|
inFlightSync: InFlightSyncSnapshot | undefined;
|
|
363
476
|
transport: TransportSnapshot | undefined;
|
|
477
|
+
lastSyncHandshakeAttempt: SyncHandshakeAttemptSnapshot;
|
|
364
478
|
};
|
|
365
479
|
}>;
|
|
366
480
|
};
|
|
@@ -378,6 +492,17 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
|
|
|
378
492
|
* every listed peer, so a device joining into an established lobby
|
|
379
493
|
* dials every knownPeer it is meant to initiate to in one pass. */
|
|
380
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;
|
|
381
506
|
/** Handle the signalling server's `peer-left` notification: a
|
|
382
507
|
* previously joined peer has closed its socket. Evict any slot we
|
|
383
508
|
* hold for that peer so a subsequent `peer-joined` from the same
|
|
@@ -406,9 +531,41 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
|
|
|
406
531
|
* dial the ones the keyring authorises that we do not already have
|
|
407
532
|
* a slot for. The periodic sweep started in {@link connect} calls
|
|
408
533
|
* this; consumers can call it manually to skip the wait after they
|
|
409
|
-
* 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)". */
|
|
410
546
|
refreshKnownPeers(): void;
|
|
411
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;
|
|
412
569
|
whenReady(): Promise<void>;
|
|
413
570
|
/**
|
|
414
571
|
* Start the adapter. Marks the adapter ready so Automerge's
|
|
@@ -428,7 +585,15 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
|
|
|
428
585
|
* keyring after their `peer-joined` notification has already fired.
|
|
429
586
|
* No-op when no keyringSource was supplied — the captured-set
|
|
430
587
|
* fallback has no live source to re-read, so the sweep would be
|
|
431
|
-
* 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. */
|
|
432
597
|
private startKnownPeersSweep;
|
|
433
598
|
private stopKnownPeersSweep;
|
|
434
599
|
/**
|
|
@@ -521,6 +686,19 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
|
|
|
521
686
|
* them — a single bad candidate must not stall the connection. */
|
|
522
687
|
private flushPendingRemoteIce;
|
|
523
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;
|
|
524
702
|
private wireDataChannel;
|
|
525
703
|
/** Refresh the per-peer {@link TransportSnapshot} for one peer by
|
|
526
704
|
* pulling {@link RTCPeerConnection.getStats} and distilling the
|
|
@@ -553,6 +731,11 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
|
|
|
553
731
|
* `inFlightSync` reassembly state worth bookkeeping. Small
|
|
554
732
|
* single-message dispatches yield but don't touch inFlightSync. */
|
|
555
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;
|
|
556
739
|
private finishInFlightSyncApply;
|
|
557
740
|
private emitSyncProgress;
|
|
558
741
|
private handleSyncFragment;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fairfox/polly",
|
|
3
|
-
"version": "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",
|