@serfab/cadre-core 1.9.0 → 1.11.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.
Files changed (119) hide show
  1. package/dist/cadre-node.d.ts +148 -44
  2. package/dist/cadre-node.js +363 -139
  3. package/dist/cadre-node.js.map +1 -1
  4. package/dist/control-authorization.d.ts +2 -2
  5. package/dist/control-authorization.js +1 -0
  6. package/dist/control-authorization.js.map +1 -1
  7. package/dist/control-cohort.d.ts +1 -1
  8. package/dist/control-cohort.js +1 -1
  9. package/dist/control-database.d.ts +105 -14
  10. package/dist/control-database.js +242 -73
  11. package/dist/control-database.js.map +1 -1
  12. package/dist/control-formation-recorder.d.ts +6 -6
  13. package/dist/control-formation-recorder.js +7 -8
  14. package/dist/control-formation-recorder.js.map +1 -1
  15. package/dist/control-schema.d.ts +1 -1
  16. package/dist/control-schema.js +1066 -979
  17. package/dist/control-schema.js.map +1 -1
  18. package/dist/control-write-retry.d.ts +21 -15
  19. package/dist/control-write-retry.js +21 -15
  20. package/dist/control-write-retry.js.map +1 -1
  21. package/dist/diagnostics/connection-path.d.ts +1 -18
  22. package/dist/diagnostics/connection-path.js +1 -20
  23. package/dist/diagnostics/connection-path.js.map +1 -1
  24. package/dist/identity-key.d.ts +0 -30
  25. package/dist/identity-key.js +7 -40
  26. package/dist/identity-key.js.map +1 -1
  27. package/dist/index.d.ts +8 -5
  28. package/dist/index.js +12 -6
  29. package/dist/index.js.map +1 -1
  30. package/dist/invitation-bootstrap.d.ts +32 -0
  31. package/dist/invitation-bootstrap.js +30 -0
  32. package/dist/invitation-bootstrap.js.map +1 -0
  33. package/dist/link-budget.d.ts +204 -119
  34. package/dist/link-budget.js +208 -120
  35. package/dist/link-budget.js.map +1 -1
  36. package/dist/membership-connection-gater.d.ts +3 -3
  37. package/dist/membership-connection-gater.js +3 -3
  38. package/dist/peer-dial.d.ts +18 -14
  39. package/dist/peer-dial.js +18 -14
  40. package/dist/peer-dial.js.map +1 -1
  41. package/dist/peer-join-backfill.d.ts +3 -3
  42. package/dist/pending-join-runner.d.ts +186 -0
  43. package/dist/pending-join-runner.js +649 -0
  44. package/dist/pending-join-runner.js.map +1 -0
  45. package/dist/push-fanout.d.ts +1 -1
  46. package/dist/push-fanout.js +1 -1
  47. package/dist/relay-stun.d.ts +24 -0
  48. package/dist/relay-stun.js +51 -0
  49. package/dist/relay-stun.js.map +1 -0
  50. package/dist/seed-bootstrap.d.ts +2 -2
  51. package/dist/seed-bootstrap.js +5 -5
  52. package/dist/strand-addr-protocol.js +2 -2
  53. package/dist/strand-formation-deadlines.d.ts +5 -5
  54. package/dist/strand-formation-deadlines.js +5 -5
  55. package/dist/strand-formation-manager.d.ts +18 -4
  56. package/dist/strand-formation-manager.js +48 -33
  57. package/dist/strand-formation-manager.js.map +1 -1
  58. package/dist/strand-formation-protocol.d.ts +71 -14
  59. package/dist/strand-formation-protocol.js +222 -47
  60. package/dist/strand-formation-protocol.js.map +1 -1
  61. package/dist/strand-formation-rejection.d.ts +94 -0
  62. package/dist/strand-formation-rejection.js +77 -0
  63. package/dist/strand-formation-rejection.js.map +1 -0
  64. package/dist/strand-instance-manager.js +4 -1
  65. package/dist/strand-instance-manager.js.map +1 -1
  66. package/dist/strand-membership-writer.js +32 -32
  67. package/dist/strand-solicitation.d.ts +11 -3
  68. package/dist/strand-solicitation.js +9 -0
  69. package/dist/strand-solicitation.js.map +1 -1
  70. package/dist/types.d.ts +103 -17
  71. package/dist/types.js +5 -9
  72. package/dist/types.js.map +1 -1
  73. package/package.json +22 -22
  74. package/src/cadre-node.ts +8654 -8400
  75. package/src/control-authorization.ts +2 -1
  76. package/src/control-cohort.ts +128 -128
  77. package/src/control-database.ts +303 -82
  78. package/src/control-formation-recorder.ts +353 -354
  79. package/src/control-read-retry.ts +216 -216
  80. package/src/control-schema.ts +1078 -991
  81. package/src/control-write-retry.ts +567 -561
  82. package/src/diagnostics/connection-path.ts +1 -29
  83. package/src/formation-approval.ts +610 -610
  84. package/src/hibernation-manager.ts +638 -638
  85. package/src/identity-key.ts +7 -62
  86. package/src/index.ts +644 -627
  87. package/src/invitation-bootstrap.ts +40 -0
  88. package/src/joined-strand-store.ts +498 -498
  89. package/src/link-budget.ts +684 -583
  90. package/src/membership-connection-gater.ts +570 -570
  91. package/src/node-local-snapshot.ts +298 -298
  92. package/src/peer-dial.ts +315 -311
  93. package/src/peer-join-backfill.ts +710 -710
  94. package/src/pending-join-runner.ts +735 -0
  95. package/src/push-fanout.ts +1 -1
  96. package/src/relay-reservation.ts +1121 -1121
  97. package/src/relay-server.ts +98 -98
  98. package/src/relay-stun.ts +58 -0
  99. package/src/seed-bootstrap.ts +1448 -1448
  100. package/src/strand-addr-protocol.ts +547 -547
  101. package/src/strand-database.ts +287 -287
  102. package/src/strand-first-sync-gate.ts +391 -391
  103. package/src/strand-formation-deadlines.ts +227 -227
  104. package/src/strand-formation-manager.ts +725 -700
  105. package/src/strand-formation-protocol.ts +1036 -802
  106. package/src/strand-formation-rejection.ts +129 -0
  107. package/src/strand-fret-addrs.ts +145 -145
  108. package/src/strand-instance-manager.ts +1789 -1786
  109. package/src/strand-membership-writer.ts +1974 -1974
  110. package/src/strand-network-state-file.ts +40 -40
  111. package/src/strand-network-state.ts +243 -243
  112. package/src/strand-revocation-enforcer.ts +654 -654
  113. package/src/strand-solicitation.ts +532 -519
  114. package/src/strand-wake-protocol.ts +401 -401
  115. package/src/types.ts +2222 -2140
  116. package/dist/diagnostics/webrtc-turn-tracker.d.ts +0 -50
  117. package/dist/diagnostics/webrtc-turn-tracker.js +0 -196
  118. package/dist/diagnostics/webrtc-turn-tracker.js.map +0 -1
  119. package/src/diagnostics/webrtc-turn-tracker.ts +0 -222
@@ -1,50 +0,0 @@
1
- /**
2
- * webrtc-turn-tracker.ts — observe whether ICE selected a TURN relay candidate
3
- * for each WebRTC session, so a TURN-relayed `/webrtc` connection can be
4
- * reclassified `relayed`/`webrtc-turn` instead of silently counted `direct`.
5
- *
6
- * `@libp2p/webrtc` keeps the underlying `RTCPeerConnection` private and never
7
- * surfaces it on the libp2p `Connection`, so the multiaddr classifier
8
- * (`connection-path.ts`) cannot see the ICE candidate types. This tracker hooks
9
- * `globalThis.RTCPeerConnection` at install time: every connection that reaches
10
- * `connectionState === 'connected'` is inspected via `getStats()`, and the
11
- * relay/not-relay verdict for the selected candidate pair is pushed onto a FIFO
12
- * queue. {@link CadreNode} drains that queue on each `connection:open` to tag the
13
- * matching peer (see {@link consume}).
14
- *
15
- * The correlation between a queued settlement and a `connection:open` is
16
- * timing-based and best-effort — it degrades gracefully (unknown → treated as not
17
- * relayed, a safe default that never produces a false `relayed`). It is inert on
18
- * Node.js / any runtime with no `RTCPeerConnection`.
19
- */
20
- export declare class TurnRelayTracker {
21
- private readonly queue;
22
- private OriginalRTCPeerConnection;
23
- /** One settlement record per connection; guards against repeated `connected` events. */
24
- private readonly observed;
25
- /**
26
- * Wrap `globalThis.RTCPeerConnection` with a subclass that records each
27
- * connection's TURN-relay verdict when it settles. Idempotent. A NOP when no
28
- * `RTCPeerConnection` exists (Node.js / no WebRTC polyfill) — the tracker stays
29
- * inert and {@link consume} always returns `null`.
30
- */
31
- install(): void;
32
- /**
33
- * Pop the most recent settled entry within `windowMs` ms of now, returning its
34
- * relay verdict (`true`/`false`); `null` when the queue has no matching entry.
35
- * Also prunes entries older than the window (they can never match a later
36
- * consume). Synchronous and safe to call from a `connection:open` handler.
37
- */
38
- consume(windowMs: number): boolean | null;
39
- /** Restore the original `RTCPeerConnection` and clear the queue. Idempotent. */
40
- dispose(): void;
41
- /**
42
- * Inspect a settled connection's stats and queue its TURN-relay verdict. Any
43
- * failure (stats API absent, rejected promise, malformed report) is caught and
44
- * recorded as `isRelay: false` — the safe default (unknown → not relayed) keeps
45
- * the connection counted rather than dropping it.
46
- */
47
- private recordSettlement;
48
- /** Drop queued entries settled before `cutoffMs` (the queue is in push order). */
49
- private pruneOlderThan;
50
- }
@@ -1,196 +0,0 @@
1
- /**
2
- * webrtc-turn-tracker.ts — observe whether ICE selected a TURN relay candidate
3
- * for each WebRTC session, so a TURN-relayed `/webrtc` connection can be
4
- * reclassified `relayed`/`webrtc-turn` instead of silently counted `direct`.
5
- *
6
- * `@libp2p/webrtc` keeps the underlying `RTCPeerConnection` private and never
7
- * surfaces it on the libp2p `Connection`, so the multiaddr classifier
8
- * (`connection-path.ts`) cannot see the ICE candidate types. This tracker hooks
9
- * `globalThis.RTCPeerConnection` at install time: every connection that reaches
10
- * `connectionState === 'connected'` is inspected via `getStats()`, and the
11
- * relay/not-relay verdict for the selected candidate pair is pushed onto a FIFO
12
- * queue. {@link CadreNode} drains that queue on each `connection:open` to tag the
13
- * matching peer (see {@link consume}).
14
- *
15
- * The correlation between a queued settlement and a `connection:open` is
16
- * timing-based and best-effort — it degrades gracefully (unknown → treated as not
17
- * relayed, a safe default that never produces a false `relayed`). It is inert on
18
- * Node.js / any runtime with no `RTCPeerConnection`.
19
- */
20
- import debug from 'debug';
21
- const log = debug('sereus:cadre:turn-tracker');
22
- /**
23
- * Hard cap on the FIFO queue. In steady state every settlement is consumed by a
24
- * `connection:open`, so the queue stays tiny; the cap only bounds a pathological
25
- * leak (settlements that never get a matching consume).
26
- */
27
- const MAX_QUEUE = 64;
28
- export class TurnRelayTracker {
29
- constructor() {
30
- this.queue = [];
31
- /** One settlement record per connection; guards against repeated `connected` events. */
32
- this.observed = new WeakSet();
33
- }
34
- /**
35
- * Wrap `globalThis.RTCPeerConnection` with a subclass that records each
36
- * connection's TURN-relay verdict when it settles. Idempotent. A NOP when no
37
- * `RTCPeerConnection` exists (Node.js / no WebRTC polyfill) — the tracker stays
38
- * inert and {@link consume} always returns `null`.
39
- */
40
- install() {
41
- if (this.OriginalRTCPeerConnection) {
42
- return; // already installed
43
- }
44
- const Original = globalThis.RTCPeerConnection;
45
- if (typeof Original !== 'function') {
46
- log('install: no globalThis.RTCPeerConnection — tracker inert (Node.js / no polyfill)');
47
- return;
48
- }
49
- this.OriginalRTCPeerConnection = Original;
50
- // eslint-disable-next-line @typescript-eslint/no-this-alias
51
- const tracker = this;
52
- class TrackedRTCPeerConnection extends Original {
53
- constructor(...args) {
54
- super(...args);
55
- try {
56
- this.addEventListener('connectionstatechange', () => {
57
- try {
58
- if (this.connectionState === 'connected' && !tracker.observed.has(this)) {
59
- tracker.observed.add(this);
60
- void tracker.recordSettlement(this);
61
- }
62
- }
63
- catch (error) {
64
- log('connectionstatechange handler failed: %o', error);
65
- }
66
- });
67
- }
68
- catch (error) {
69
- log('install: failed to attach connectionstatechange listener: %o', error);
70
- }
71
- }
72
- }
73
- globalThis.RTCPeerConnection = TrackedRTCPeerConnection;
74
- log('install: wrapped globalThis.RTCPeerConnection');
75
- }
76
- /**
77
- * Pop the most recent settled entry within `windowMs` ms of now, returning its
78
- * relay verdict (`true`/`false`); `null` when the queue has no matching entry.
79
- * Also prunes entries older than the window (they can never match a later
80
- * consume). Synchronous and safe to call from a `connection:open` handler.
81
- */
82
- consume(windowMs) {
83
- const now = Date.now();
84
- let result = null;
85
- // Walk newest → oldest; the most recent in-window settlement is the best
86
- // correlation for a just-opened connection.
87
- for (let i = this.queue.length - 1; i >= 0; i--) {
88
- if (now - this.queue[i].settledAtMs <= windowMs) {
89
- result = this.queue[i].isRelay;
90
- this.queue.splice(i, 1);
91
- break;
92
- }
93
- }
94
- this.pruneOlderThan(now - windowMs);
95
- return result;
96
- }
97
- /** Restore the original `RTCPeerConnection` and clear the queue. Idempotent. */
98
- dispose() {
99
- if (this.OriginalRTCPeerConnection) {
100
- globalThis.RTCPeerConnection = this.OriginalRTCPeerConnection;
101
- this.OriginalRTCPeerConnection = undefined;
102
- }
103
- this.queue.length = 0;
104
- }
105
- /**
106
- * Inspect a settled connection's stats and queue its TURN-relay verdict. Any
107
- * failure (stats API absent, rejected promise, malformed report) is caught and
108
- * recorded as `isRelay: false` — the safe default (unknown → not relayed) keeps
109
- * the connection counted rather than dropping it.
110
- */
111
- async recordSettlement(pc) {
112
- // Defaults to not-relayed (the safe "unknown" verdict); only the success path
113
- // below overrides it, so a getStats failure leaves it false.
114
- let isRelay = false;
115
- try {
116
- const report = await pc.getStats();
117
- isRelay = selectedCandidatePairIsRelay(statsToMap(report));
118
- }
119
- catch (error) {
120
- log('recordSettlement: getStats failed, keeping not-relayed default: %o', error);
121
- }
122
- this.queue.push({ settledAtMs: Date.now(), isRelay });
123
- if (this.queue.length > MAX_QUEUE) {
124
- this.queue.shift();
125
- }
126
- }
127
- /** Drop queued entries settled before `cutoffMs` (the queue is in push order). */
128
- pruneOlderThan(cutoffMs) {
129
- while (this.queue.length > 0 && this.queue[0].settledAtMs < cutoffMs) {
130
- this.queue.shift();
131
- }
132
- }
133
- }
134
- /** Materialise an `RTCStatsReport` into a plain map keyed by stats id. */
135
- function statsToMap(report) {
136
- const map = new Map();
137
- report.forEach((value, key) => {
138
- map.set(key, value);
139
- });
140
- return map;
141
- }
142
- /**
143
- * Whether the selected ICE candidate pair used a TURN relay candidate on either
144
- * end. The pair is the transport's `selectedCandidatePairId` when present, else
145
- * the nominated succeeded pair, else any succeeded pair.
146
- */
147
- function selectedCandidatePairIsRelay(stats) {
148
- const pair = findSelectedPair(stats);
149
- if (!pair) {
150
- return false;
151
- }
152
- const local = pair.localCandidateId ? stats.get(pair.localCandidateId) : undefined;
153
- const remote = pair.remoteCandidateId ? stats.get(pair.remoteCandidateId) : undefined;
154
- return isRelayCandidate(local) || isRelayCandidate(remote);
155
- }
156
- function isRelayCandidate(stat) {
157
- return stat?.type === 'local-candidate' || stat?.type === 'remote-candidate'
158
- ? stat.candidateType === 'relay'
159
- : false;
160
- }
161
- function findSelectedPair(stats) {
162
- const selectedId = selectedPairIdFromTransport(stats);
163
- if (selectedId) {
164
- const stat = stats.get(selectedId);
165
- if (stat?.type === 'candidate-pair') {
166
- return stat;
167
- }
168
- }
169
- let succeeded;
170
- for (const stat of stats.values()) {
171
- if (stat.type !== 'candidate-pair') {
172
- continue;
173
- }
174
- const pair = stat;
175
- if (pair.state !== 'succeeded') {
176
- continue;
177
- }
178
- if (pair.nominated) {
179
- return pair; // nominated succeeded pair wins outright
180
- }
181
- succeeded ?? (succeeded = pair);
182
- }
183
- return succeeded;
184
- }
185
- function selectedPairIdFromTransport(stats) {
186
- for (const stat of stats.values()) {
187
- if (stat.type === 'transport') {
188
- const id = stat.selectedCandidatePairId;
189
- if (typeof id === 'string') {
190
- return id;
191
- }
192
- }
193
- }
194
- return undefined;
195
- }
196
- //# sourceMappingURL=webrtc-turn-tracker.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"webrtc-turn-tracker.js","sourceRoot":"","sources":["../../src/diagnostics/webrtc-turn-tracker.ts"],"names":[],"mappings":"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"}
@@ -1,222 +0,0 @@
1
- /**
2
- * webrtc-turn-tracker.ts — observe whether ICE selected a TURN relay candidate
3
- * for each WebRTC session, so a TURN-relayed `/webrtc` connection can be
4
- * reclassified `relayed`/`webrtc-turn` instead of silently counted `direct`.
5
- *
6
- * `@libp2p/webrtc` keeps the underlying `RTCPeerConnection` private and never
7
- * surfaces it on the libp2p `Connection`, so the multiaddr classifier
8
- * (`connection-path.ts`) cannot see the ICE candidate types. This tracker hooks
9
- * `globalThis.RTCPeerConnection` at install time: every connection that reaches
10
- * `connectionState === 'connected'` is inspected via `getStats()`, and the
11
- * relay/not-relay verdict for the selected candidate pair is pushed onto a FIFO
12
- * queue. {@link CadreNode} drains that queue on each `connection:open` to tag the
13
- * matching peer (see {@link consume}).
14
- *
15
- * The correlation between a queued settlement and a `connection:open` is
16
- * timing-based and best-effort — it degrades gracefully (unknown → treated as not
17
- * relayed, a safe default that never produces a false `relayed`). It is inert on
18
- * Node.js / any runtime with no `RTCPeerConnection`.
19
- */
20
-
21
- import debug from 'debug';
22
-
23
- const log = debug('sereus:cadre:turn-tracker');
24
-
25
- /** A single observed WebRTC settlement: when it connected and whether ICE relayed. */
26
- interface SettlementEntry {
27
- settledAtMs: number;
28
- isRelay: boolean;
29
- }
30
-
31
- /**
32
- * Hard cap on the FIFO queue. In steady state every settlement is consumed by a
33
- * `connection:open`, so the queue stays tiny; the cap only bounds a pathological
34
- * leak (settlements that never get a matching consume).
35
- */
36
- const MAX_QUEUE = 64;
37
-
38
- export class TurnRelayTracker {
39
- private readonly queue: SettlementEntry[] = [];
40
- private OriginalRTCPeerConnection: typeof RTCPeerConnection | undefined;
41
- /** One settlement record per connection; guards against repeated `connected` events. */
42
- private readonly observed = new WeakSet<RTCPeerConnection>();
43
-
44
- /**
45
- * Wrap `globalThis.RTCPeerConnection` with a subclass that records each
46
- * connection's TURN-relay verdict when it settles. Idempotent. A NOP when no
47
- * `RTCPeerConnection` exists (Node.js / no WebRTC polyfill) — the tracker stays
48
- * inert and {@link consume} always returns `null`.
49
- */
50
- install(): void {
51
- if (this.OriginalRTCPeerConnection) {
52
- return; // already installed
53
- }
54
- const Original = globalThis.RTCPeerConnection;
55
- if (typeof Original !== 'function') {
56
- log('install: no globalThis.RTCPeerConnection — tracker inert (Node.js / no polyfill)');
57
- return;
58
- }
59
- this.OriginalRTCPeerConnection = Original;
60
-
61
- // eslint-disable-next-line @typescript-eslint/no-this-alias
62
- const tracker = this;
63
- class TrackedRTCPeerConnection extends Original {
64
- constructor(...args: ConstructorParameters<typeof RTCPeerConnection>) {
65
- super(...args);
66
- try {
67
- this.addEventListener('connectionstatechange', () => {
68
- try {
69
- if (this.connectionState === 'connected' && !tracker.observed.has(this)) {
70
- tracker.observed.add(this);
71
- void tracker.recordSettlement(this);
72
- }
73
- } catch (error) {
74
- log('connectionstatechange handler failed: %o', error);
75
- }
76
- });
77
- } catch (error) {
78
- log('install: failed to attach connectionstatechange listener: %o', error);
79
- }
80
- }
81
- }
82
-
83
- globalThis.RTCPeerConnection = TrackedRTCPeerConnection as typeof RTCPeerConnection;
84
- log('install: wrapped globalThis.RTCPeerConnection');
85
- }
86
-
87
- /**
88
- * Pop the most recent settled entry within `windowMs` ms of now, returning its
89
- * relay verdict (`true`/`false`); `null` when the queue has no matching entry.
90
- * Also prunes entries older than the window (they can never match a later
91
- * consume). Synchronous and safe to call from a `connection:open` handler.
92
- */
93
- consume(windowMs: number): boolean | null {
94
- const now = Date.now();
95
- let result: boolean | null = null;
96
- // Walk newest → oldest; the most recent in-window settlement is the best
97
- // correlation for a just-opened connection.
98
- for (let i = this.queue.length - 1; i >= 0; i--) {
99
- if (now - this.queue[i]!.settledAtMs <= windowMs) {
100
- result = this.queue[i]!.isRelay;
101
- this.queue.splice(i, 1);
102
- break;
103
- }
104
- }
105
- this.pruneOlderThan(now - windowMs);
106
- return result;
107
- }
108
-
109
- /** Restore the original `RTCPeerConnection` and clear the queue. Idempotent. */
110
- dispose(): void {
111
- if (this.OriginalRTCPeerConnection) {
112
- globalThis.RTCPeerConnection = this.OriginalRTCPeerConnection;
113
- this.OriginalRTCPeerConnection = undefined;
114
- }
115
- this.queue.length = 0;
116
- }
117
-
118
- /**
119
- * Inspect a settled connection's stats and queue its TURN-relay verdict. Any
120
- * failure (stats API absent, rejected promise, malformed report) is caught and
121
- * recorded as `isRelay: false` — the safe default (unknown → not relayed) keeps
122
- * the connection counted rather than dropping it.
123
- */
124
- private async recordSettlement(pc: RTCPeerConnection): Promise<void> {
125
- // Defaults to not-relayed (the safe "unknown" verdict); only the success path
126
- // below overrides it, so a getStats failure leaves it false.
127
- let isRelay = false;
128
- try {
129
- const report = await pc.getStats();
130
- isRelay = selectedCandidatePairIsRelay(statsToMap(report));
131
- } catch (error) {
132
- log('recordSettlement: getStats failed, keeping not-relayed default: %o', error);
133
- }
134
- this.queue.push({ settledAtMs: Date.now(), isRelay });
135
- if (this.queue.length > MAX_QUEUE) {
136
- this.queue.shift();
137
- }
138
- }
139
-
140
- /** Drop queued entries settled before `cutoffMs` (the queue is in push order). */
141
- private pruneOlderThan(cutoffMs: number): void {
142
- while (this.queue.length > 0 && this.queue[0]!.settledAtMs < cutoffMs) {
143
- this.queue.shift();
144
- }
145
- }
146
- }
147
-
148
- /** Materialise an `RTCStatsReport` into a plain map keyed by stats id. */
149
- function statsToMap(report: RTCStatsReport): Map<string, RTCStats> {
150
- const map = new Map<string, RTCStats>();
151
- report.forEach((value: RTCStats, key: string) => {
152
- map.set(key, value);
153
- });
154
- return map;
155
- }
156
-
157
- /**
158
- * Whether the selected ICE candidate pair used a TURN relay candidate on either
159
- * end. The pair is the transport's `selectedCandidatePairId` when present, else
160
- * the nominated succeeded pair, else any succeeded pair.
161
- */
162
- function selectedCandidatePairIsRelay(stats: Map<string, RTCStats>): boolean {
163
- const pair = findSelectedPair(stats);
164
- if (!pair) {
165
- return false;
166
- }
167
- const local = pair.localCandidateId ? stats.get(pair.localCandidateId) : undefined;
168
- const remote = pair.remoteCandidateId ? stats.get(pair.remoteCandidateId) : undefined;
169
- return isRelayCandidate(local) || isRelayCandidate(remote);
170
- }
171
-
172
- /**
173
- * Minimal shape of a local/remote ICE candidate stats entry. Declared locally
174
- * rather than relying on a DOM `RTCIceCandidateStats` lib type (absent in some
175
- * TS lib versions); only `candidateType` is read.
176
- */
177
- interface IceCandidateStats extends RTCStats {
178
- candidateType?: string;
179
- }
180
-
181
- function isRelayCandidate(stat: RTCStats | undefined): boolean {
182
- return stat?.type === 'local-candidate' || stat?.type === 'remote-candidate'
183
- ? (stat as IceCandidateStats).candidateType === 'relay'
184
- : false;
185
- }
186
-
187
- function findSelectedPair(stats: Map<string, RTCStats>): RTCIceCandidatePairStats | undefined {
188
- const selectedId = selectedPairIdFromTransport(stats);
189
- if (selectedId) {
190
- const stat = stats.get(selectedId);
191
- if (stat?.type === 'candidate-pair') {
192
- return stat as RTCIceCandidatePairStats;
193
- }
194
- }
195
- let succeeded: RTCIceCandidatePairStats | undefined;
196
- for (const stat of stats.values()) {
197
- if (stat.type !== 'candidate-pair') {
198
- continue;
199
- }
200
- const pair = stat as RTCIceCandidatePairStats;
201
- if (pair.state !== 'succeeded') {
202
- continue;
203
- }
204
- if (pair.nominated) {
205
- return pair; // nominated succeeded pair wins outright
206
- }
207
- succeeded ??= pair;
208
- }
209
- return succeeded;
210
- }
211
-
212
- function selectedPairIdFromTransport(stats: Map<string, RTCStats>): string | undefined {
213
- for (const stat of stats.values()) {
214
- if (stat.type === 'transport') {
215
- const id = (stat as RTCTransportStats).selectedCandidatePairId;
216
- if (typeof id === 'string') {
217
- return id;
218
- }
219
- }
220
- }
221
- return undefined;
222
- }