@fairfox/polly 0.53.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
@@ -89,6 +104,20 @@ export interface CreateMeshClientOptions {
89
104
  * `examples/mesh-recovery-pair`). Forwarded straight to
90
105
  * {@link MeshWebRTCAdapterOptions.knownPeersRefreshIntervalMs}. */
91
106
  knownPeersRefreshIntervalMs?: MeshWebRTCAdapterOptions["knownPeersRefreshIntervalMs"];
107
+ /** Forward of {@link MeshWebRTCAdapterOptions.syncYieldEnabled}.
108
+ * Defaults to `true`. The `examples/mesh-large-initial-sync`
109
+ * example flips this to `false` when `POLLY_104_DISABLE_FIX=1` is
110
+ * set, to demonstrate the pre-#104 tight-loop behaviour against
111
+ * post-fix polly. Production callers should leave this at the
112
+ * default. */
113
+ syncYieldEnabled?: MeshWebRTCAdapterOptions["syncYieldEnabled"];
114
+ /** Forward of
115
+ * {@link MeshWebRTCAdapterOptions.syncFragmentChunkSizeOverride}.
116
+ * Production callers should leave this undefined. The
117
+ * `examples/mesh-large-initial-sync` example passes 64 KiB when
118
+ * `POLLY_104_DISABLE_FIX=1` to recreate the pre-#104
119
+ * fragmentation bug. */
120
+ syncFragmentChunkSizeOverride?: MeshWebRTCAdapterOptions["syncFragmentChunkSizeOverride"];
92
121
  };
93
122
  /** The local peer's keyring — one of three shapes:
94
123
  *
@@ -172,6 +201,14 @@ export interface MeshClient {
172
201
  * harness can answer "is the mesh layer in a known good state"
173
202
  * without instrumenting polly internals. Polly issue #103 item 7. */
174
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>;
175
212
  /** Close the signalling WebSocket, tear down every RTCPeerConnection,
176
213
  * and shut the Repo cleanly. Idempotent. */
177
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. */
@@ -111,6 +132,115 @@ export interface MeshWebRTCAdapterOptions {
111
132
  * (e.g. `werift` or `@roamhq/wrtc`) when running outside a browser, or
112
133
  * to use a custom subclass for tests or instrumentation. */
113
134
  RTCPeerConnection?: typeof RTCPeerConnection;
135
+ /** When `true` (the default), the adapter yields to the event loop
136
+ * between the points where a large initial sync would otherwise hold
137
+ * the main thread for tens of seconds: between each batch of
138
+ * fragmented `RTCDataChannel.send` calls on the sender side, and at
139
+ * the boundary between reassembling a sync message and dispatching
140
+ * it (deserialise → MeshNetworkAdapter unwrap → Automerge
141
+ * `applyChanges`) on the receiver side. Set to `false` to recover
142
+ * the pre-#104 tight-loop behaviour; this is the configuration the
143
+ * `POLLY_104_DISABLE_FIX=1` falsification path in
144
+ * `examples/mesh-large-initial-sync` uses to demonstrate the bug
145
+ * against post-fix polly. Production callers should leave this at
146
+ * the default. */
147
+ syncYieldEnabled?: boolean;
148
+ /** Override the sync fragment chunk size. Defaults to
149
+ * {@link SYNC_FRAGMENT_CHUNK_SIZE} (60 KiB), which leaves header
150
+ * overhead inside werift's hard 64 KiB max-message-size cap.
151
+ * Setting this to 64 KiB recreates the pre-#104 fragmentation bug,
152
+ * where peer A's outbound fragments overshoot the cap and werift
153
+ * rejects them silently — sync stalls forever. Used by the
154
+ * `POLLY_104_DISABLE_FIX=1` falsification path. Production callers
155
+ * should leave this at the default. */
156
+ syncFragmentChunkSizeOverride?: number;
157
+ }
158
+ /** Payload of the polly-specific `"sync-progress"` event emitted by
159
+ * {@link MeshWebRTCAdapter}. Consumers can subscribe via the adapter's
160
+ * standard `.on()` surface (the same one that carries `peer-candidate`
161
+ * and `peer-disconnected`) to observe fragment receive and dispatch
162
+ * activity in real time, without polling
163
+ * {@link MeshWebRTCAdapter.getPeerStateSnapshot}. Polly issue #104
164
+ * item 7. */
165
+ export interface SyncProgressEvent {
166
+ /** Remote peer the fragment or dispatch is for. */
167
+ peerId: string;
168
+ /** Lifecycle stage. `fragment-received` fires for each chunk that
169
+ * arrives during reassembly; `dispatch-applied` fires once the
170
+ * reassembled message has been emitted upward to Automerge. */
171
+ kind: "fragment-received" | "dispatch-applied";
172
+ /** Bytes carried by the chunk that triggered the event. Zero for
173
+ * `dispatch-applied`. */
174
+ bytesDelta: number;
175
+ /** Running total of fragments received for the current reassembly. */
176
+ chunksReceived: number;
177
+ /** Running total of bytes received for the current reassembly. */
178
+ bytesReceived: number;
179
+ /** Number of reassembled messages whose dispatch has been scheduled
180
+ * but not yet emitted upward to Automerge. */
181
+ applyBacklog: number;
182
+ /** `performance.now()` at event emission. */
183
+ at: number;
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
+ /** Per-peer view of an in-flight initial sync. Populated by
223
+ * {@link MeshWebRTCAdapter.handleSyncFragment} as fragments of a
224
+ * single reassembly arrive, and reset to `undefined` once the
225
+ * reassembled message has been dispatched. Exposed verbatim through
226
+ * {@link MeshWebRTCAdapter.getPeerStateSnapshot} so a consumer
227
+ * harness can observe progress mid-stream. Polly issue #104 item 7. */
228
+ export interface InFlightSyncSnapshot {
229
+ /** Fragments received for the current reassembly. Cleared once
230
+ * reassembly completes. */
231
+ chunksReceived: number;
232
+ /** Bytes received across the fragments of the current reassembly.
233
+ * The reassembled message will be slightly smaller than this sum
234
+ * because each fragment carries a small header. */
235
+ bytesReceived: number;
236
+ /** `performance.now()` value at the last fragment arrival. */
237
+ lastChunkAt: number;
238
+ /** Count of reassembled messages whose dispatch has been
239
+ * scheduled but not yet run. With the receiver-side `setTimeout(0)`
240
+ * yield enabled, this is normally 0 or 1; with the yield
241
+ * disabled (the falsification path) dispatch runs synchronously
242
+ * and this stays 0. */
243
+ applyBacklog: number;
114
244
  }
115
245
  /**
116
246
  * Automerge-Repo NetworkAdapter backed by real WebRTC data channels.
@@ -120,6 +250,8 @@ export interface MeshWebRTCAdapterOptions {
120
250
  export declare class MeshWebRTCAdapter extends NetworkAdapter {
121
251
  readonly signaling: MeshSignalingClient;
122
252
  readonly iceServers: RTCIceServer[];
253
+ readonly iceTransportPolicy: RTCIceTransportPolicy | undefined;
254
+ private readonly iceRelayEnforcement;
123
255
  readonly dataChannelLabel: string;
124
256
  /** Peers this adapter is willing to dial. Mutable so callers that pair
125
257
  * a new device after construction (e.g. a CLI `add-device` process whose
@@ -142,6 +274,17 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
142
274
  * {@link MeshWebRTCAdapterOptions.knownPeersRefreshIntervalMs} at
143
275
  * construction. Defaults to 2000ms; tests override to 100–250ms. */
144
276
  private readonly knownPeersRefreshIntervalMs;
277
+ /** When `true`, the sender side awaits between batches of fragment
278
+ * sends and the receiver side schedules dispatch via `setTimeout(0)`
279
+ * so the JS event loop can drain timers (including the consumer's
280
+ * own setInterval-based liveness probes) between large-message
281
+ * apply calls. Defaults to `true`; set to `false` only by the
282
+ * `POLLY_104_DISABLE_FIX` falsification path. */
283
+ private readonly syncYieldEnabled;
284
+ /** Resolved chunk size for fragmenting oversized messages.
285
+ * Defaults to {@link SYNC_FRAGMENT_CHUNK_SIZE}; can be overridden
286
+ * via {@link MeshWebRTCAdapterOptions.syncFragmentChunkSizeOverride}. */
287
+ private readonly syncFragmentChunkSize;
145
288
  /** Peers currently visible in the signalling roster — populated by
146
289
  * {@link handlePeersPresent} / {@link handlePeerJoined} and pruned by
147
290
  * {@link handlePeerLeft}. Read by {@link addKnownPeer} to decide
@@ -216,6 +359,8 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
216
359
  dataChannelState: string;
217
360
  pendingSendCount: number;
218
361
  pendingRemoteIceCount: number;
362
+ inFlightSync: InFlightSyncSnapshot | undefined;
363
+ transport: TransportSnapshot | undefined;
219
364
  };
220
365
  }>;
221
366
  };
@@ -293,13 +438,30 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
293
438
  * queued until the data channel is open.
294
439
  */
295
440
  send(message: Message): void;
441
+ /** Number of consecutive fragment sends after which the sender
442
+ * yields to the macrotask queue when {@link syncYieldEnabled} is on.
443
+ * 8 × 64 KiB = 512 KiB of bytes between yields — small enough that
444
+ * a 5 MB sync produces many yield points (and the JS event loop
445
+ * drains the consumer's `setInterval` liveness probes between
446
+ * them), large enough that the per-yield overhead does not dominate
447
+ * the wire cost. */
448
+ private static readonly SEND_YIELD_EVERY_N_FRAGMENTS;
296
449
  /** Send raw wire bytes, fragmenting if they exceed the SCTP maxMessageSize
297
450
  * cap. The default RTCDataChannel limit is 256 KiB in current Chrome and
298
451
  * werift; oversized sends either throw, drop silently, or stall the
299
452
  * channel, none of which surface as an error to the caller. Fragments
300
453
  * use the same length-prefixed JSON header wire format as ordinary
301
454
  * messages but carry a `sync-fragment` type that the receive path
302
- * detects and reassembles before deserialising. */
455
+ * detects and reassembles before deserialising.
456
+ *
457
+ * When {@link MeshWebRTCAdapterOptions.syncYieldEnabled} is true (the
458
+ * default), the loop awaits the macrotask queue every
459
+ * {@link SEND_YIELD_EVERY_N_FRAGMENTS} fragments so the JS event
460
+ * loop drains between batches — without this, a 5–8 MB initial
461
+ * sync produces 78–125 back-to-back `RTCDataChannel.send` calls in
462
+ * a tight loop, starving anything else on the main thread (polly
463
+ * issue #104, sender side). When the option is false the legacy
464
+ * tight-loop shape is preserved for the falsification path. */
303
465
  private sendBytesMaybeFragmented;
304
466
  /**
305
467
  * Entry point the signalling client calls when it receives a signal
@@ -310,6 +472,44 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
310
472
  * method.
311
473
  */
312
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;
313
513
  private createInitiatingSlot;
314
514
  private initiateOffer;
315
515
  private handleOffer;
@@ -322,8 +522,47 @@ export declare class MeshWebRTCAdapter extends NetworkAdapter {
322
522
  private flushPendingRemoteIce;
323
523
  private wireConnection;
324
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>>;
325
540
  private dispatchMessage;
541
+ /** Hand a deserialised Automerge message off to whoever is listening
542
+ * on this adapter's `"message"` event. When
543
+ * {@link MeshWebRTCAdapterOptions.syncYieldEnabled} is true (the
544
+ * default), the emit runs on a fresh macrotask so the crypto-unwrap
545
+ * and Automerge `applyChanges` chain downstream of this method does
546
+ * not sit on the same JS stack frame as the wire `onmessage` callback
547
+ * — that's the receiver-side starvation site polly issue #104
548
+ * documents. When the option is false the emit is synchronous,
549
+ * recovering the pre-fix shape used by the falsification path.
550
+ *
551
+ * The `viaFragmentPath` argument tags whether this dispatch came out
552
+ * of a reassembled fragment chain; only those carry an
553
+ * `inFlightSync` reassembly state worth bookkeeping. Small
554
+ * single-message dispatches yield but don't touch inFlightSync. */
555
+ private scheduleEmitMessage;
556
+ private finishInFlightSyncApply;
557
+ private emitSyncProgress;
326
558
  private handleSyncFragment;
559
+ /** Dispatch a reassembled fragment payload back through
560
+ * {@link dispatchMessage}, but tagged so the
561
+ * {@link scheduleEmitMessage} path knows it owes a
562
+ * `finishInFlightSyncApply` afterwards. Synchronous re-entry into
563
+ * `dispatchMessage` would lose that signal, so the post-fragment
564
+ * deserialise+emit is inlined here. */
565
+ private dispatchReassembled;
327
566
  /** Peer IDs with an open data channel, suitable for blob requests. */
328
567
  get connectedPeerIds(): string[];
329
568
  /** Send a pre-serialised blob message to a specific peer. Returns false
@@ -17,12 +17,23 @@
17
17
  * does not mistake a sync fragment for a blob chunk.
18
18
  */
19
19
  /** Maximum bytes a single channel.send may carry without fragmentation.
20
- * Chosen well below the 256 KiB SCTP cap so the framing header for a
21
- * single chunk cannot push the wire frame over the limit. Matches the
22
- * blob-transfer chunk size so the two transports have a consistent
23
- * per-message footprint on the data channel. */
20
+ * Werift (the node-side WebRTC implementation polly recommends for
21
+ * CLI/daemon use) enforces a hard 64 KiB (65536 bytes) maxMessageSize
22
+ * on its RTCDataChannel anything larger is rejected with a
23
+ * `max-message-size exceeded` error and silently drops the channel
24
+ * for that send. Chrome's SCTP cap is 256 KiB and would tolerate
25
+ * larger frames, but the threshold is chosen to fit inside werift's
26
+ * cap WITH per-fragment header overhead included so a single mesh
27
+ * deployment works on both transports. Matches the blob-transfer
28
+ * chunk size so the two transports have a consistent per-message
29
+ * footprint on the data channel. */
24
30
  export declare const SYNC_FRAGMENT_THRESHOLD: number;
25
- /** Chunk size used when a message exceeds the threshold. */
31
+ /** Chunk size used when a message exceeds the threshold. Left at the
32
+ * same value as {@link SYNC_FRAGMENT_THRESHOLD} so the framing
33
+ * header (a JSON-encoded `SyncFragmentHeader` of ~90 bytes plus a
34
+ * 4-byte length prefix) does not push any fragment over werift's
35
+ * 64 KiB wire limit — see polly issue #104 for the failure mode
36
+ * this guards against. */
26
37
  export declare const SYNC_FRAGMENT_CHUNK_SIZE: number;
27
38
  export interface SyncFragmentHeader {
28
39
  type: "sync-fragment";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fairfox/polly",
3
- "version": "0.53.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",