@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.
package/dist/src/mesh.js
CHANGED
|
@@ -1849,8 +1849,8 @@ import {
|
|
|
1849
1849
|
} from "@automerge/automerge-repo/slim";
|
|
1850
1850
|
|
|
1851
1851
|
// src/shared/lib/sync-fragment.ts
|
|
1852
|
-
var SYNC_FRAGMENT_THRESHOLD =
|
|
1853
|
-
var SYNC_FRAGMENT_CHUNK_SIZE =
|
|
1852
|
+
var SYNC_FRAGMENT_THRESHOLD = 60 * 1024;
|
|
1853
|
+
var SYNC_FRAGMENT_CHUNK_SIZE = 60 * 1024;
|
|
1854
1854
|
function serialiseSyncFragment(header, data) {
|
|
1855
1855
|
const headerBytes = new TextEncoder().encode(JSON.stringify(header));
|
|
1856
1856
|
const size = 4 + headerBytes.length + data.length;
|
|
@@ -1933,6 +1933,132 @@ function findSubarray2(haystack, needle) {
|
|
|
1933
1933
|
}
|
|
1934
1934
|
|
|
1935
1935
|
// src/shared/lib/mesh-webrtc-adapter.ts
|
|
1936
|
+
async function collectTransportSnapshot(connection, lastDataChannelError) {
|
|
1937
|
+
const at = performance.now();
|
|
1938
|
+
let report;
|
|
1939
|
+
try {
|
|
1940
|
+
report = await connection.getStats();
|
|
1941
|
+
} catch {
|
|
1942
|
+
return emptyTransportSnapshot(lastDataChannelError, at);
|
|
1943
|
+
}
|
|
1944
|
+
const parsed = partitionStats(report);
|
|
1945
|
+
const selectedPair = selectActivePair(parsed);
|
|
1946
|
+
const selectedCandidatePair = selectedPair ? buildCandidatePairView(selectedPair, parsed.localCands, parsed.remoteCands) : undefined;
|
|
1947
|
+
return {
|
|
1948
|
+
selectedCandidatePair,
|
|
1949
|
+
retransmittedPacketsSent: parsed.retransmittedPacketsSent,
|
|
1950
|
+
retransmittedBytesSent: parsed.retransmittedBytesSent,
|
|
1951
|
+
lastDataChannelError,
|
|
1952
|
+
at
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
function emptyTransportSnapshot(lastDataChannelError, at) {
|
|
1956
|
+
return {
|
|
1957
|
+
selectedCandidatePair: undefined,
|
|
1958
|
+
retransmittedPacketsSent: undefined,
|
|
1959
|
+
retransmittedBytesSent: undefined,
|
|
1960
|
+
lastDataChannelError,
|
|
1961
|
+
at
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
function partitionStats(report) {
|
|
1965
|
+
const out = {
|
|
1966
|
+
localCands: new Map,
|
|
1967
|
+
remoteCands: new Map,
|
|
1968
|
+
pairs: new Map,
|
|
1969
|
+
selectedPairId: undefined,
|
|
1970
|
+
retransmittedPacketsSent: undefined,
|
|
1971
|
+
retransmittedBytesSent: undefined
|
|
1972
|
+
};
|
|
1973
|
+
const iter = report.values?.() ?? [];
|
|
1974
|
+
for (const raw of iter) {
|
|
1975
|
+
if (!raw || typeof raw !== "object")
|
|
1976
|
+
continue;
|
|
1977
|
+
const stat = raw;
|
|
1978
|
+
ingestStat(stat, out);
|
|
1979
|
+
}
|
|
1980
|
+
return out;
|
|
1981
|
+
}
|
|
1982
|
+
function ingestStat(stat, out) {
|
|
1983
|
+
const id = String(stat["id"]);
|
|
1984
|
+
switch (stat["type"]) {
|
|
1985
|
+
case "local-candidate":
|
|
1986
|
+
out.localCands.set(id, stat);
|
|
1987
|
+
return;
|
|
1988
|
+
case "remote-candidate":
|
|
1989
|
+
out.remoteCands.set(id, stat);
|
|
1990
|
+
return;
|
|
1991
|
+
case "candidate-pair":
|
|
1992
|
+
out.pairs.set(id, stat);
|
|
1993
|
+
return;
|
|
1994
|
+
case "transport":
|
|
1995
|
+
ingestTransport(stat, out);
|
|
1996
|
+
return;
|
|
1997
|
+
case "data-channel":
|
|
1998
|
+
ingestDataChannel(stat, out);
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
function ingestTransport(stat, out) {
|
|
2003
|
+
const selectedId = stat["selectedCandidatePairId"];
|
|
2004
|
+
if (typeof selectedId === "string")
|
|
2005
|
+
out.selectedPairId = selectedId;
|
|
2006
|
+
const rp = stat["retransmittedPacketsSent"];
|
|
2007
|
+
const rb = stat["retransmittedBytesSent"];
|
|
2008
|
+
if (typeof rp === "number")
|
|
2009
|
+
out.retransmittedPacketsSent = rp;
|
|
2010
|
+
if (typeof rb === "number")
|
|
2011
|
+
out.retransmittedBytesSent = rb;
|
|
2012
|
+
}
|
|
2013
|
+
function ingestDataChannel(stat, out) {
|
|
2014
|
+
const rp = stat["retransmittedPacketsSent"];
|
|
2015
|
+
const rb = stat["retransmittedBytesSent"];
|
|
2016
|
+
if (out.retransmittedPacketsSent === undefined && typeof rp === "number") {
|
|
2017
|
+
out.retransmittedPacketsSent = rp;
|
|
2018
|
+
}
|
|
2019
|
+
if (out.retransmittedBytesSent === undefined && typeof rb === "number") {
|
|
2020
|
+
out.retransmittedBytesSent = rb;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
function selectActivePair(parsed) {
|
|
2024
|
+
if (parsed.selectedPairId) {
|
|
2025
|
+
const named = parsed.pairs.get(parsed.selectedPairId);
|
|
2026
|
+
if (named)
|
|
2027
|
+
return named;
|
|
2028
|
+
}
|
|
2029
|
+
for (const pair of parsed.pairs.values()) {
|
|
2030
|
+
if (pair["nominated"])
|
|
2031
|
+
return pair;
|
|
2032
|
+
}
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
function serialiseSlotView(slot) {
|
|
2036
|
+
return {
|
|
2037
|
+
signalingState: slot.connection.signalingState,
|
|
2038
|
+
iceConnectionState: slot.connection.iceConnectionState,
|
|
2039
|
+
connectionState: slot.connection.connectionState,
|
|
2040
|
+
dataChannelState: slot.channel?.readyState ?? "no-channel",
|
|
2041
|
+
pendingSendCount: slot.pendingSends.length,
|
|
2042
|
+
pendingRemoteIceCount: slot.pendingRemoteIce.length,
|
|
2043
|
+
inFlightSync: slot.inFlightSync ? { ...slot.inFlightSync } : undefined,
|
|
2044
|
+
transport: slot.transport ? {
|
|
2045
|
+
...slot.transport,
|
|
2046
|
+
selectedCandidatePair: slot.transport.selectedCandidatePair ? { ...slot.transport.selectedCandidatePair } : undefined
|
|
2047
|
+
} : undefined
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
function buildCandidatePairView(pair, localCands, remoteCands) {
|
|
2051
|
+
const local = localCands.get(String(pair["localCandidateId"]));
|
|
2052
|
+
const remote = remoteCands.get(String(pair["remoteCandidateId"]));
|
|
2053
|
+
return {
|
|
2054
|
+
localCandidateType: String(local?.["candidateType"] ?? "?"),
|
|
2055
|
+
remoteCandidateType: String(remote?.["candidateType"] ?? "?"),
|
|
2056
|
+
state: String(pair["state"] ?? ""),
|
|
2057
|
+
nominated: Boolean(pair["nominated"]),
|
|
2058
|
+
bytesSent: Number(pair["bytesSent"] ?? 0),
|
|
2059
|
+
bytesReceived: Number(pair["bytesReceived"] ?? 0)
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
1936
2062
|
var DEFAULT_ICE_SERVERS = [
|
|
1937
2063
|
{ urls: "stun:stun.l.google.com:19302" },
|
|
1938
2064
|
{ urls: "stun:stun1.l.google.com:19302" }
|
|
@@ -1941,11 +2067,15 @@ var DEFAULT_ICE_SERVERS = [
|
|
|
1941
2067
|
class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
1942
2068
|
signaling;
|
|
1943
2069
|
iceServers;
|
|
2070
|
+
iceTransportPolicy;
|
|
2071
|
+
iceRelayEnforcement;
|
|
1944
2072
|
dataChannelLabel;
|
|
1945
2073
|
knownPeers;
|
|
1946
2074
|
keyringSource;
|
|
1947
2075
|
knownPeersRefreshTimer;
|
|
1948
2076
|
knownPeersRefreshIntervalMs;
|
|
2077
|
+
syncYieldEnabled;
|
|
2078
|
+
syncFragmentChunkSize;
|
|
1949
2079
|
presentPeers = new Set;
|
|
1950
2080
|
localPeerId;
|
|
1951
2081
|
RTCPeerConnectionCtor;
|
|
@@ -1969,10 +2099,14 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
1969
2099
|
super();
|
|
1970
2100
|
this.signaling = options.signaling;
|
|
1971
2101
|
this.iceServers = options.iceServers ?? DEFAULT_ICE_SERVERS;
|
|
2102
|
+
this.iceTransportPolicy = options.iceTransportPolicy;
|
|
2103
|
+
this.iceRelayEnforcement = options.iceRelayEnforcement ?? true;
|
|
1972
2104
|
this.dataChannelLabel = options.dataChannelLabel ?? "polly-mesh";
|
|
1973
2105
|
this.knownPeers = new Set(options.knownPeerIds ?? []);
|
|
1974
2106
|
this.keyringSource = options.keyringSource;
|
|
1975
2107
|
this.knownPeersRefreshIntervalMs = options.knownPeersRefreshIntervalMs ?? 2000;
|
|
2108
|
+
this.syncYieldEnabled = options.syncYieldEnabled ?? true;
|
|
2109
|
+
this.syncFragmentChunkSize = options.syncFragmentChunkSizeOverride ?? SYNC_FRAGMENT_CHUNK_SIZE;
|
|
1976
2110
|
this.localPeerId = options.peerId;
|
|
1977
2111
|
const PC = options.RTCPeerConnection ?? globalThis.RTCPeerConnection;
|
|
1978
2112
|
if (typeof PC !== "function") {
|
|
@@ -2004,14 +2138,7 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2004
2138
|
peerId,
|
|
2005
2139
|
knownInKeyring: knownPeerSet.has(peerId),
|
|
2006
2140
|
presentInSignalling: this.presentPeers.has(peerId),
|
|
2007
|
-
slot: slot ?
|
|
2008
|
-
signalingState: slot.connection.signalingState,
|
|
2009
|
-
iceConnectionState: slot.connection.iceConnectionState,
|
|
2010
|
-
connectionState: slot.connection.connectionState,
|
|
2011
|
-
dataChannelState: slot.channel?.readyState ?? "no-channel",
|
|
2012
|
-
pendingSendCount: slot.pendingSends.length,
|
|
2013
|
-
pendingRemoteIceCount: slot.pendingRemoteIce.length
|
|
2014
|
-
} : undefined
|
|
2141
|
+
slot: slot ? serialiseSlotView(slot) : undefined
|
|
2015
2142
|
});
|
|
2016
2143
|
}
|
|
2017
2144
|
return {
|
|
@@ -2133,15 +2260,29 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2133
2260
|
slot.pendingSends.push(bytes);
|
|
2134
2261
|
}
|
|
2135
2262
|
}
|
|
2136
|
-
|
|
2263
|
+
static SEND_YIELD_EVERY_N_FRAGMENTS = 8;
|
|
2264
|
+
async sendBytesMaybeFragmented(channel, bytes) {
|
|
2137
2265
|
if (bytes.length <= SYNC_FRAGMENT_THRESHOLD) {
|
|
2138
2266
|
channel.send(bytes);
|
|
2139
2267
|
return;
|
|
2140
2268
|
}
|
|
2141
2269
|
const id = crypto.randomUUID();
|
|
2142
|
-
const fragments = chunkSyncMessage(bytes, id);
|
|
2270
|
+
const fragments = chunkSyncMessage(bytes, id, this.syncFragmentChunkSize);
|
|
2271
|
+
if (!this.syncYieldEnabled) {
|
|
2272
|
+
for (const fragment of fragments) {
|
|
2273
|
+
channel.send(fragment);
|
|
2274
|
+
}
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2277
|
+
let i = 0;
|
|
2143
2278
|
for (const fragment of fragments) {
|
|
2144
2279
|
channel.send(fragment);
|
|
2280
|
+
i += 1;
|
|
2281
|
+
if (i % MeshWebRTCAdapter.SEND_YIELD_EVERY_N_FRAGMENTS === 0 && i < fragments.length) {
|
|
2282
|
+
await new Promise((resolve) => {
|
|
2283
|
+
setTimeout(resolve, 0);
|
|
2284
|
+
});
|
|
2285
|
+
}
|
|
2145
2286
|
}
|
|
2146
2287
|
}
|
|
2147
2288
|
handleSignal(fromPeerId, rawPayload) {
|
|
@@ -2161,15 +2302,71 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2161
2302
|
return;
|
|
2162
2303
|
}
|
|
2163
2304
|
}
|
|
2305
|
+
buildRtcConfiguration() {
|
|
2306
|
+
const config = { iceServers: this.iceServers };
|
|
2307
|
+
if (this.iceTransportPolicy !== undefined) {
|
|
2308
|
+
config.iceTransportPolicy = this.iceTransportPolicy;
|
|
2309
|
+
}
|
|
2310
|
+
return config;
|
|
2311
|
+
}
|
|
2312
|
+
isRelayCandidateInit(init) {
|
|
2313
|
+
const candidateStr = init.candidate ?? "";
|
|
2314
|
+
const m = candidateStr.match(/\btyp\s+(\S+)/i);
|
|
2315
|
+
return m?.[1]?.toLowerCase() === "relay";
|
|
2316
|
+
}
|
|
2317
|
+
shouldSendCandidate(candidate) {
|
|
2318
|
+
if (!this.iceRelayEnforcement)
|
|
2319
|
+
return true;
|
|
2320
|
+
if (this.iceTransportPolicy !== "relay")
|
|
2321
|
+
return true;
|
|
2322
|
+
const typed = candidate.type;
|
|
2323
|
+
const match = candidate.candidate.match(/\btyp\s+(\S+)/i);
|
|
2324
|
+
const candidateType = (typed ?? match?.[1] ?? "").toLowerCase();
|
|
2325
|
+
return candidateType === "relay";
|
|
2326
|
+
}
|
|
2327
|
+
filterSdpCandidatesByPolicy(sdp) {
|
|
2328
|
+
if (!this.iceRelayEnforcement)
|
|
2329
|
+
return sdp;
|
|
2330
|
+
if (this.iceTransportPolicy !== "relay")
|
|
2331
|
+
return sdp;
|
|
2332
|
+
if (!sdp)
|
|
2333
|
+
return sdp;
|
|
2334
|
+
const lines = sdp.split(/\r?\n/);
|
|
2335
|
+
const filtered = [];
|
|
2336
|
+
for (const line of lines) {
|
|
2337
|
+
if (!line.startsWith("a=candidate:")) {
|
|
2338
|
+
filtered.push(line);
|
|
2339
|
+
continue;
|
|
2340
|
+
}
|
|
2341
|
+
const m = line.match(/\btyp\s+(\S+)/i);
|
|
2342
|
+
if (m?.[1]?.toLowerCase() === "relay")
|
|
2343
|
+
filtered.push(line);
|
|
2344
|
+
}
|
|
2345
|
+
return filtered.join(`\r
|
|
2346
|
+
`);
|
|
2347
|
+
}
|
|
2348
|
+
applySdpPolicyFilter(description) {
|
|
2349
|
+
if (!this.iceRelayEnforcement)
|
|
2350
|
+
return description;
|
|
2351
|
+
if (this.iceTransportPolicy !== "relay")
|
|
2352
|
+
return description;
|
|
2353
|
+
const filtered = this.filterSdpCandidatesByPolicy(description.sdp);
|
|
2354
|
+
if (filtered === description.sdp)
|
|
2355
|
+
return description;
|
|
2356
|
+
return { ...description, sdp: filtered };
|
|
2357
|
+
}
|
|
2164
2358
|
createInitiatingSlot(targetId) {
|
|
2165
|
-
const connection = new this.RTCPeerConnectionCtor(
|
|
2359
|
+
const connection = new this.RTCPeerConnectionCtor(this.buildRtcConfiguration());
|
|
2166
2360
|
const channel = connection.createDataChannel(this.dataChannelLabel, { ordered: true });
|
|
2167
2361
|
const slot = {
|
|
2168
2362
|
connection,
|
|
2169
2363
|
channel,
|
|
2170
2364
|
pendingSends: [],
|
|
2171
2365
|
pendingFragments: new Map,
|
|
2172
|
-
pendingRemoteIce: []
|
|
2366
|
+
pendingRemoteIce: [],
|
|
2367
|
+
inFlightSync: undefined,
|
|
2368
|
+
transport: undefined,
|
|
2369
|
+
lastDataChannelError: undefined
|
|
2173
2370
|
};
|
|
2174
2371
|
this.slots.set(targetId, slot);
|
|
2175
2372
|
this.wireConnection(targetId, connection);
|
|
@@ -2180,7 +2377,15 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2180
2377
|
async initiateOffer(targetId, connection) {
|
|
2181
2378
|
const offer = await connection.createOffer();
|
|
2182
2379
|
await connection.setLocalDescription(offer);
|
|
2183
|
-
|
|
2380
|
+
const localOffer = connection.localDescription ?? offer;
|
|
2381
|
+
const sdpToSend = this.applySdpPolicyFilter({
|
|
2382
|
+
type: localOffer.type,
|
|
2383
|
+
sdp: localOffer.sdp ?? offer.sdp
|
|
2384
|
+
});
|
|
2385
|
+
this.signaling.sendSignal(targetId, {
|
|
2386
|
+
kind: "offer",
|
|
2387
|
+
sdp: sdpToSend
|
|
2388
|
+
});
|
|
2184
2389
|
}
|
|
2185
2390
|
async handleOffer(fromPeerId, sdp) {
|
|
2186
2391
|
const existing = this.slots.get(fromPeerId);
|
|
@@ -2193,13 +2398,16 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2193
2398
|
existing.connection.close();
|
|
2194
2399
|
this.slots.delete(fromPeerId);
|
|
2195
2400
|
}
|
|
2196
|
-
const connection = new this.RTCPeerConnectionCtor(
|
|
2401
|
+
const connection = new this.RTCPeerConnectionCtor(this.buildRtcConfiguration());
|
|
2197
2402
|
const slot = {
|
|
2198
2403
|
connection,
|
|
2199
2404
|
channel: undefined,
|
|
2200
2405
|
pendingSends: [],
|
|
2201
2406
|
pendingFragments: new Map,
|
|
2202
|
-
pendingRemoteIce: []
|
|
2407
|
+
pendingRemoteIce: [],
|
|
2408
|
+
inFlightSync: undefined,
|
|
2409
|
+
transport: undefined,
|
|
2410
|
+
lastDataChannelError: undefined
|
|
2203
2411
|
};
|
|
2204
2412
|
this.slots.set(fromPeerId, slot);
|
|
2205
2413
|
this.wireConnection(fromPeerId, connection);
|
|
@@ -2207,13 +2415,18 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2207
2415
|
slot.channel = event.channel;
|
|
2208
2416
|
this.wireDataChannel(fromPeerId, event.channel);
|
|
2209
2417
|
};
|
|
2210
|
-
await connection.setRemoteDescription(sdp);
|
|
2418
|
+
await connection.setRemoteDescription(this.applySdpPolicyFilter(sdp));
|
|
2211
2419
|
await this.flushPendingRemoteIce(slot);
|
|
2212
2420
|
const answer = await connection.createAnswer();
|
|
2213
2421
|
await connection.setLocalDescription(answer);
|
|
2422
|
+
const localAnswer = connection.localDescription ?? answer;
|
|
2423
|
+
const sdpToSend = this.applySdpPolicyFilter({
|
|
2424
|
+
type: localAnswer.type,
|
|
2425
|
+
sdp: localAnswer.sdp ?? answer.sdp
|
|
2426
|
+
});
|
|
2214
2427
|
this.signaling.sendSignal(fromPeerId, {
|
|
2215
2428
|
kind: "answer",
|
|
2216
|
-
sdp:
|
|
2429
|
+
sdp: sdpToSend
|
|
2217
2430
|
});
|
|
2218
2431
|
}
|
|
2219
2432
|
async handleAnswer(fromPeerId, sdp) {
|
|
@@ -2222,13 +2435,15 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2222
2435
|
return;
|
|
2223
2436
|
if (slot.connection.signalingState !== "have-local-offer")
|
|
2224
2437
|
return;
|
|
2225
|
-
await slot.connection.setRemoteDescription(sdp);
|
|
2438
|
+
await slot.connection.setRemoteDescription(this.applySdpPolicyFilter(sdp));
|
|
2226
2439
|
await this.flushPendingRemoteIce(slot);
|
|
2227
2440
|
}
|
|
2228
2441
|
async handleIceCandidate(fromPeerId, candidate) {
|
|
2229
2442
|
const slot = this.slots.get(fromPeerId);
|
|
2230
2443
|
if (!slot)
|
|
2231
2444
|
return;
|
|
2445
|
+
if (this.iceRelayEnforcement && this.iceTransportPolicy === "relay" && !this.isRelayCandidateInit(candidate))
|
|
2446
|
+
return;
|
|
2232
2447
|
if (slot.connection.remoteDescription === null) {
|
|
2233
2448
|
slot.pendingRemoteIce.push(candidate);
|
|
2234
2449
|
return;
|
|
@@ -2250,12 +2465,14 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2250
2465
|
}
|
|
2251
2466
|
wireConnection(peerId, connection) {
|
|
2252
2467
|
connection.onicecandidate = (event) => {
|
|
2253
|
-
if (event.candidate)
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2468
|
+
if (!event.candidate)
|
|
2469
|
+
return;
|
|
2470
|
+
if (!this.shouldSendCandidate(event.candidate))
|
|
2471
|
+
return;
|
|
2472
|
+
this.signaling.sendSignal(peerId, {
|
|
2473
|
+
kind: "ice",
|
|
2474
|
+
candidate: event.candidate.toJSON()
|
|
2475
|
+
});
|
|
2259
2476
|
};
|
|
2260
2477
|
connection.onconnectionstatechange = () => {
|
|
2261
2478
|
const state = connection.connectionState;
|
|
@@ -2294,6 +2511,34 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2294
2511
|
slot.channel = undefined;
|
|
2295
2512
|
}
|
|
2296
2513
|
};
|
|
2514
|
+
channel.onerror = (event) => {
|
|
2515
|
+
const slot = this.slots.get(peerId);
|
|
2516
|
+
if (!slot)
|
|
2517
|
+
return;
|
|
2518
|
+
const ev = event;
|
|
2519
|
+
const message = typeof ev.error?.message === "string" ? ev.error.message : typeof ev.message === "string" ? ev.message : "unknown data channel error";
|
|
2520
|
+
slot.lastDataChannelError = message;
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
2523
|
+
async refreshTransportStats(peerId) {
|
|
2524
|
+
const slot = this.slots.get(peerId);
|
|
2525
|
+
if (!slot)
|
|
2526
|
+
return;
|
|
2527
|
+
const snapshot = await collectTransportSnapshot(slot.connection, slot.lastDataChannelError);
|
|
2528
|
+
slot.transport = snapshot;
|
|
2529
|
+
return snapshot;
|
|
2530
|
+
}
|
|
2531
|
+
async refreshAllTransportStats() {
|
|
2532
|
+
const out = new Map;
|
|
2533
|
+
const peerIds = [...this.slots.keys()];
|
|
2534
|
+
const results = await Promise.all(peerIds.map((id) => this.refreshTransportStats(id)));
|
|
2535
|
+
for (let i = 0;i < peerIds.length; i += 1) {
|
|
2536
|
+
const r = results[i];
|
|
2537
|
+
const id = peerIds[i];
|
|
2538
|
+
if (r && id !== undefined)
|
|
2539
|
+
out.set(id, r);
|
|
2540
|
+
}
|
|
2541
|
+
return out;
|
|
2297
2542
|
}
|
|
2298
2543
|
dispatchMessage(fromPeerId, bytes) {
|
|
2299
2544
|
try {
|
|
@@ -2310,9 +2555,56 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2310
2555
|
return;
|
|
2311
2556
|
}
|
|
2312
2557
|
const message = this.deserialiseMessage(bytes);
|
|
2313
|
-
this.
|
|
2558
|
+
this.scheduleEmitMessage(fromPeerId, message, false);
|
|
2314
2559
|
} catch {}
|
|
2315
2560
|
}
|
|
2561
|
+
scheduleEmitMessage(fromPeerId, message, viaFragmentPath) {
|
|
2562
|
+
if (!this.syncYieldEnabled) {
|
|
2563
|
+
this.emit("message", message);
|
|
2564
|
+
if (viaFragmentPath) {
|
|
2565
|
+
this.finishInFlightSyncApply(fromPeerId);
|
|
2566
|
+
}
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
if (viaFragmentPath) {
|
|
2570
|
+
const slot = this.slots.get(fromPeerId);
|
|
2571
|
+
if (slot?.inFlightSync) {
|
|
2572
|
+
slot.inFlightSync.applyBacklog += 1;
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
setTimeout(() => {
|
|
2576
|
+
try {
|
|
2577
|
+
this.emit("message", message);
|
|
2578
|
+
} finally {
|
|
2579
|
+
if (viaFragmentPath) {
|
|
2580
|
+
this.finishInFlightSyncApply(fromPeerId);
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
}, 0);
|
|
2584
|
+
}
|
|
2585
|
+
finishInFlightSyncApply(fromPeerId) {
|
|
2586
|
+
const slot = this.slots.get(fromPeerId);
|
|
2587
|
+
if (!slot?.inFlightSync)
|
|
2588
|
+
return;
|
|
2589
|
+
slot.inFlightSync.applyBacklog = Math.max(0, slot.inFlightSync.applyBacklog - 1);
|
|
2590
|
+
this.emitSyncProgress(fromPeerId, "dispatch-applied", 0);
|
|
2591
|
+
if (slot.inFlightSync.applyBacklog === 0 && slot.pendingFragments.size === 0) {
|
|
2592
|
+
slot.inFlightSync = undefined;
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
emitSyncProgress(fromPeerId, kind, bytesDelta) {
|
|
2596
|
+
const slot = this.slots.get(fromPeerId);
|
|
2597
|
+
const inFlightSync = slot?.inFlightSync;
|
|
2598
|
+
this.emit("sync-progress", {
|
|
2599
|
+
peerId: fromPeerId,
|
|
2600
|
+
kind,
|
|
2601
|
+
bytesDelta,
|
|
2602
|
+
chunksReceived: inFlightSync?.chunksReceived ?? 0,
|
|
2603
|
+
bytesReceived: inFlightSync?.bytesReceived ?? 0,
|
|
2604
|
+
applyBacklog: inFlightSync?.applyBacklog ?? 0,
|
|
2605
|
+
at: performance.now()
|
|
2606
|
+
});
|
|
2607
|
+
}
|
|
2316
2608
|
handleSyncFragment(fromPeerId, bytes) {
|
|
2317
2609
|
const parsed = parseSyncFragment(bytes);
|
|
2318
2610
|
if (!parsed)
|
|
@@ -2327,11 +2619,46 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2327
2619
|
slot.pendingFragments.set(header.id, entry);
|
|
2328
2620
|
}
|
|
2329
2621
|
entry.chunks.set(header.index, data.slice());
|
|
2622
|
+
if (!slot.inFlightSync) {
|
|
2623
|
+
slot.inFlightSync = {
|
|
2624
|
+
chunksReceived: 0,
|
|
2625
|
+
bytesReceived: 0,
|
|
2626
|
+
lastChunkAt: performance.now(),
|
|
2627
|
+
applyBacklog: 0
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
slot.inFlightSync.chunksReceived += 1;
|
|
2631
|
+
slot.inFlightSync.bytesReceived += data.byteLength;
|
|
2632
|
+
slot.inFlightSync.lastChunkAt = performance.now();
|
|
2633
|
+
this.emitSyncProgress(fromPeerId, "fragment-received", data.byteLength);
|
|
2330
2634
|
if (entry.chunks.size < entry.total)
|
|
2331
2635
|
return;
|
|
2332
2636
|
slot.pendingFragments.delete(header.id);
|
|
2333
2637
|
const reassembled = reassembleSyncFragments(entry.chunks, entry.total);
|
|
2334
|
-
this.
|
|
2638
|
+
if (!this.syncYieldEnabled) {
|
|
2639
|
+
this.dispatchReassembled(fromPeerId, reassembled);
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
setTimeout(() => {
|
|
2643
|
+
this.dispatchReassembled(fromPeerId, reassembled);
|
|
2644
|
+
}, 0);
|
|
2645
|
+
}
|
|
2646
|
+
dispatchReassembled(fromPeerId, bytes) {
|
|
2647
|
+
try {
|
|
2648
|
+
if (this.onBlobMessage && isBlobMessageType(bytes)) {
|
|
2649
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
2650
|
+
const headerLen = view.getUint32(0, false);
|
|
2651
|
+
const header = JSON.parse(new TextDecoder().decode(bytes.subarray(4, 4 + headerLen)));
|
|
2652
|
+
const data = bytes.subarray(4 + headerLen);
|
|
2653
|
+
this.onBlobMessage(fromPeerId, header, data);
|
|
2654
|
+
this.finishInFlightSyncApply(fromPeerId);
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2657
|
+
const message = this.deserialiseMessage(bytes);
|
|
2658
|
+
this.scheduleEmitMessage(fromPeerId, message, true);
|
|
2659
|
+
} catch {
|
|
2660
|
+
this.finishInFlightSyncApply(fromPeerId);
|
|
2661
|
+
}
|
|
2335
2662
|
}
|
|
2336
2663
|
get connectedPeerIds() {
|
|
2337
2664
|
const ids = [];
|
|
@@ -2411,6 +2738,12 @@ async function createMeshClient(options) {
|
|
|
2411
2738
|
knownPeerIds,
|
|
2412
2739
|
keyringSource,
|
|
2413
2740
|
...resolvedIceServers !== undefined && { iceServers: resolvedIceServers },
|
|
2741
|
+
...options.rtc?.iceTransportPolicy !== undefined && {
|
|
2742
|
+
iceTransportPolicy: options.rtc.iceTransportPolicy
|
|
2743
|
+
},
|
|
2744
|
+
...options.rtc?.iceRelayEnforcement !== undefined && {
|
|
2745
|
+
iceRelayEnforcement: options.rtc.iceRelayEnforcement
|
|
2746
|
+
},
|
|
2414
2747
|
...options.rtc?.dataChannelLabel !== undefined && {
|
|
2415
2748
|
dataChannelLabel: options.rtc.dataChannelLabel
|
|
2416
2749
|
},
|
|
@@ -2419,6 +2752,12 @@ async function createMeshClient(options) {
|
|
|
2419
2752
|
},
|
|
2420
2753
|
...options.rtc?.knownPeersRefreshIntervalMs !== undefined && {
|
|
2421
2754
|
knownPeersRefreshIntervalMs: options.rtc.knownPeersRefreshIntervalMs
|
|
2755
|
+
},
|
|
2756
|
+
...options.rtc?.syncYieldEnabled !== undefined && {
|
|
2757
|
+
syncYieldEnabled: options.rtc.syncYieldEnabled
|
|
2758
|
+
},
|
|
2759
|
+
...options.rtc?.syncFragmentChunkSizeOverride !== undefined && {
|
|
2760
|
+
syncFragmentChunkSizeOverride: options.rtc.syncFragmentChunkSizeOverride
|
|
2422
2761
|
}
|
|
2423
2762
|
};
|
|
2424
2763
|
let webrtcAdapter;
|
|
@@ -2479,6 +2818,11 @@ async function createMeshClient(options) {
|
|
|
2479
2818
|
}
|
|
2480
2819
|
return webrtcAdapter.getPeerStateSnapshot();
|
|
2481
2820
|
},
|
|
2821
|
+
refreshTransportStats: async () => {
|
|
2822
|
+
if (!webrtcAdapter)
|
|
2823
|
+
return;
|
|
2824
|
+
await webrtcAdapter.refreshAllTransportStats();
|
|
2825
|
+
},
|
|
2482
2826
|
close: async () => {
|
|
2483
2827
|
signaling.close();
|
|
2484
2828
|
webrtcAdapter?.disconnect();
|
|
@@ -2893,4 +3237,4 @@ export {
|
|
|
2893
3237
|
$meshCounter
|
|
2894
3238
|
};
|
|
2895
3239
|
|
|
2896
|
-
//# debugId=
|
|
3240
|
+
//# debugId=15751E988ED47F0264756E2164756E21
|