@decentnetwork/peer 0.1.109 → 0.1.111
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/compat/filetransfer.d.ts +4 -1
- package/dist/compat/filetransfer.js +49 -11
- package/dist/peer.js +24 -12
- package/package.json +1 -1
|
@@ -41,12 +41,15 @@ export declare function parseFileData(p: Uint8Array): {
|
|
|
41
41
|
export type FtSend = (friendId: string, packetId: number, payload: Uint8Array) => Promise<unknown>;
|
|
42
42
|
export type FtEmit = (event: string, payload: Record<string, unknown>) => void;
|
|
43
43
|
export type FtIsLanPath = (friendId: string) => boolean;
|
|
44
|
+
export type FtPathKind = "lan" | "udp-direct" | "relay";
|
|
45
|
+
export type FtPathKindFn = (friendId: string) => FtPathKind;
|
|
44
46
|
export declare class FileTransferManager {
|
|
45
47
|
#private;
|
|
46
48
|
private readonly send;
|
|
47
49
|
private readonly emit;
|
|
48
50
|
private readonly isLanPath;
|
|
49
|
-
|
|
51
|
+
private readonly pathKind;
|
|
52
|
+
constructor(send: FtSend, emit: FtEmit, isLanPath?: FtIsLanPath, pathKind?: FtPathKindFn);
|
|
50
53
|
/** Enable resumable receives: partials are persisted under `dir` and matched
|
|
51
54
|
* on re-offer by content-hash fileId. Unset = in-memory only (no resume). */
|
|
52
55
|
setResumeDir(dir: string): void;
|
|
@@ -209,15 +209,17 @@ export class FileTransferManager {
|
|
|
209
209
|
send;
|
|
210
210
|
emit;
|
|
211
211
|
isLanPath;
|
|
212
|
+
pathKind;
|
|
212
213
|
#sending = new Map();
|
|
213
214
|
#receiving = new Map();
|
|
214
215
|
// When set, incoming transfers persist their contiguous prefix here so a
|
|
215
216
|
// dropped/restarted transfer resumes instead of restarting (断点续传).
|
|
216
217
|
#resumeDir;
|
|
217
|
-
constructor(send, emit, isLanPath = () => false) {
|
|
218
|
+
constructor(send, emit, isLanPath = () => false, pathKind = (friendId) => (this.isLanPath(friendId) ? "lan" : "relay")) {
|
|
218
219
|
this.send = send;
|
|
219
220
|
this.emit = emit;
|
|
220
221
|
this.isLanPath = isLanPath;
|
|
222
|
+
this.pathKind = pathKind;
|
|
221
223
|
}
|
|
222
224
|
/** Enable resumable receives: partials are persisted under `dir` and matched
|
|
223
225
|
* on re-offer by content-hash fileId. Unset = in-memory only (no resume). */
|
|
@@ -279,7 +281,7 @@ export class FileTransferManager {
|
|
|
279
281
|
minRttMs: Infinity, srttMs: 0, probeOffset: -1, probeSentMs: 0,
|
|
280
282
|
paceRateBps: lanPath ? LAN_PACE_INIT_BPS : PACE_INIT_BPS,
|
|
281
283
|
paceTokens: 0, lastPaceMs: nowMs(),
|
|
282
|
-
btlBwBps: 0, bwSamples: [], fullBwRounds: 0, startupQueueRounds: 0, bbrPhase: 0, roundCount: 0,
|
|
284
|
+
btlBwBps: 0, bwSamples: [], fullBwBps: 0, fullBwRounds: 0, startupQueueRounds: 0, bbrPhase: 0, roundCount: 0,
|
|
283
285
|
rateMarkMs: nowMs(), rateMarkAcked: 0, lowRttCount: 0, lowRttMinMs: Infinity, lastCcLogMs: 0,
|
|
284
286
|
lanPath,
|
|
285
287
|
dbgLanNow: lanPath ? 1 : 0, dbgSampleMs: 0, dbgRoundRate: 0, dbgMeasuredRate: 0,
|
|
@@ -783,12 +785,14 @@ export class FileTransferManager {
|
|
|
783
785
|
// One paced round completed (the timed frontier byte's ack landed).
|
|
784
786
|
const rtt = nowT - st.probeSentMs;
|
|
785
787
|
st.probeOffset = -1;
|
|
788
|
+
const pathKindAtProbe = this.pathKind(friendId);
|
|
786
789
|
// A single very-low sample can be a batched/late ACK that makes a probe
|
|
787
790
|
// look newer than it is. But several consecutive low samples mean the
|
|
788
|
-
// path genuinely improved
|
|
789
|
-
//
|
|
790
|
-
//
|
|
791
|
-
//
|
|
791
|
+
// path genuinely improved when we are moving onto a validated LAN path.
|
|
792
|
+
// Do NOT do this on public UDP-direct: cross-border ACK compression can
|
|
793
|
+
// produce several falsely-low samples, and rebasing RTprop there shrinks
|
|
794
|
+
// the BDP window to the floor, collapsing a good remote transfer to
|
|
795
|
+
// tens of KB/s.
|
|
792
796
|
const lowSample = st.srttMs > 0 && rtt > 0 && rtt < st.srttMs * 0.4;
|
|
793
797
|
if (lowSample) {
|
|
794
798
|
st.lowRttCount++;
|
|
@@ -798,7 +802,7 @@ export class FileTransferManager {
|
|
|
798
802
|
st.lowRttCount = 0;
|
|
799
803
|
st.lowRttMinMs = Infinity;
|
|
800
804
|
}
|
|
801
|
-
const promoteLowPath = lowSample && st.lowRttCount >= 3;
|
|
805
|
+
const promoteLowPath = pathKindAtProbe === "lan" && lowSample && st.lowRttCount >= 3;
|
|
802
806
|
const acceptedRtt = promoteLowPath ? st.lowRttMinMs : rtt;
|
|
803
807
|
if (promoteLowPath) {
|
|
804
808
|
st.srttMs = acceptedRtt;
|
|
@@ -821,7 +825,9 @@ export class FileTransferManager {
|
|
|
821
825
|
const roundRate = ((st.acked - st.rateMarkAcked) * 1000) / sampleMs;
|
|
822
826
|
st.rateMarkMs = nowT;
|
|
823
827
|
st.rateMarkAcked = st.acked;
|
|
824
|
-
const
|
|
828
|
+
const pathKind = pathKindAtProbe;
|
|
829
|
+
const lanNow = pathKind === "lan";
|
|
830
|
+
const udpDirectNow = pathKind === "udp-direct";
|
|
825
831
|
// On a paced LAN flow, sustainable delivery cannot suddenly be
|
|
826
832
|
// many times faster than the bytes we are putting on the wire.
|
|
827
833
|
// Such a sample is delayed/coalesced ACKs draining, not new
|
|
@@ -861,15 +867,46 @@ export class FileTransferManager {
|
|
|
861
867
|
st.lanPath = lanNow;
|
|
862
868
|
const rawQueue = st.minRttMs !== Infinity ? acceptedRtt - st.minRttMs : 0;
|
|
863
869
|
st.dbgRawQueue = Math.round(rawQueue);
|
|
864
|
-
if (!lanNow) {
|
|
865
|
-
//
|
|
870
|
+
if (!lanNow && !udpDirectNow) {
|
|
871
|
+
// TCP-relay-only file traffic must remain conservative. A short
|
|
866
872
|
// ACK burst can wildly overstate bandwidth there; allowing LAN's
|
|
867
873
|
// exponential STARTUP filled the TCP relay with an 8.7 s queue.
|
|
868
|
-
//
|
|
874
|
+
// A confirmed public UDP-direct path is different: it is the old
|
|
875
|
+
// long-haul file path with FEC and BBR-style pacing, so do not
|
|
876
|
+
// pin it to 120-240 KB/s.
|
|
869
877
|
st.bbrPhase = 2;
|
|
870
878
|
const gain = PACE_CYCLE_GAINS[st.roundCount % PACE_CYCLE_GAINS.length];
|
|
871
879
|
st.paceRateBps = Math.max(PACE_INIT_BPS, Math.min(PACE_INIT_BPS * 2, st.btlBwBps * gain * lossMult));
|
|
872
880
|
}
|
|
881
|
+
else if (udpDirectNow) {
|
|
882
|
+
// Public UDP direct: restore the pre-LAN-regression remote path.
|
|
883
|
+
// It keeps FEC loss compensation and paced BBR startup/probe, but
|
|
884
|
+
// does NOT use LAN's aggressive 2 MB/s initial probe or ACK-burst
|
|
885
|
+
// sample cap. Queue growth still exits STARTUP quickly, so this
|
|
886
|
+
// does not recreate the deep TCP-relay bufferbloat issue.
|
|
887
|
+
if (st.bbrPhase === 0) {
|
|
888
|
+
if (st.fullBwBps === 0 || st.btlBwBps >= st.fullBwBps * 1.20) {
|
|
889
|
+
st.fullBwBps = st.btlBwBps;
|
|
890
|
+
st.fullBwRounds = 0;
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
st.fullBwRounds++;
|
|
894
|
+
}
|
|
895
|
+
if (st.fullBwRounds >= 3 || rawQueue > 350) {
|
|
896
|
+
st.bbrPhase = 2;
|
|
897
|
+
}
|
|
898
|
+
if (st.bbrPhase === 0) {
|
|
899
|
+
st.paceRateBps = Math.max(st.paceRateBps * 1.4, st.btlBwBps * 2.0 * lossMult);
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
st.paceRateBps = Math.max(PACE_INIT_BPS * 0.5, st.btlBwBps * lossMult);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
else {
|
|
906
|
+
const gain = PACE_CYCLE_GAINS[st.roundCount % PACE_CYCLE_GAINS.length];
|
|
907
|
+
st.paceRateBps = Math.max(PACE_INIT_BPS * 0.5, st.btlBwBps * gain * lossMult);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
873
910
|
else if (st.bbrPhase === 0) {
|
|
874
911
|
// BBR full-bandwidth detection: remain in STARTUP while delivered
|
|
875
912
|
// bandwidth grows. A relative RTT threshold is unusable on LAN —
|
|
@@ -961,6 +998,7 @@ export class FileTransferManager {
|
|
|
961
998
|
` pace=${Math.round(st.paceRateBps)}Bps bw=${Math.round(st.btlBwBps)}Bps` +
|
|
962
999
|
` cwnd=${st.cwnd} phase=${st.bbrPhase} slow=${st.fullBwRounds}` +
|
|
963
1000
|
` loss=${(st.lossEwma * 100).toFixed(1)}% rounds=${st.roundCount}` +
|
|
1001
|
+
` path=${pathKindAtProbe}` +
|
|
964
1002
|
` lanNow=${st.dbgLanNow} lanPath=${st.lanPath ? 1 : 0}` +
|
|
965
1003
|
` sampleMs=${st.dbgSampleMs} roundRate=${st.dbgRoundRate}` +
|
|
966
1004
|
` measuredRate=${st.dbgMeasuredRate} rawQueue=${st.dbgRawQueue}` +
|
package/dist/peer.js
CHANGED
|
@@ -230,6 +230,16 @@ export class Peer {
|
|
|
230
230
|
}, (event, payload) => { this.#events.emit(event, payload); }, (friendId) => {
|
|
231
231
|
const session = this.#friendSessions.get(friendId);
|
|
232
232
|
return !!session?.lanRemoteHost && session.remote?.host === session.lanRemoteHost;
|
|
233
|
+
}, (friendId) => {
|
|
234
|
+
const session = this.#friendSessions.get(friendId);
|
|
235
|
+
const realUdpRemote = session?.remote && !session.remote.host?.startsWith("tcp:") && session.remote.port !== 0;
|
|
236
|
+
if (realUdpRemote && session.lanRemoteHost && session.remote?.host === session.lanRemoteHost)
|
|
237
|
+
return "lan";
|
|
238
|
+
if (realUdpRemote &&
|
|
239
|
+
session.lastUdpRecvMs !== undefined &&
|
|
240
|
+
Date.now() - session.lastUdpRecvMs < 4_000)
|
|
241
|
+
return "udp-direct";
|
|
242
|
+
return "relay";
|
|
233
243
|
});
|
|
234
244
|
#keyPair;
|
|
235
245
|
#udp = new UdpTransport();
|
|
@@ -4012,15 +4022,16 @@ export class Peer {
|
|
|
4012
4022
|
Date.now() - s.lastUdpRecvMs < 4_000;
|
|
4013
4023
|
const forceFileRelay = (this.#fileRelayNegotiationUntil.get(friendId) ?? 0) > Date.now() &&
|
|
4014
4024
|
!(s?.lanRemoteHost && s.remote?.host === s.lanRemoteHost);
|
|
4015
|
-
//
|
|
4016
|
-
//
|
|
4017
|
-
//
|
|
4018
|
-
//
|
|
4019
|
-
//
|
|
4020
|
-
//
|
|
4021
|
-
//
|
|
4022
|
-
|
|
4023
|
-
|
|
4025
|
+
// File DATA/FEC is reliable at the file layer: FEC + ACK/retransmit repair
|
|
4026
|
+
// loss. It should use a fresh decrypted UDP-direct path, including public
|
|
4027
|
+
// long-haul peers. A previous LAN fix accidentally required a LAN lock here,
|
|
4028
|
+
// which forced China/US file sends back onto TCP relay and destroyed the
|
|
4029
|
+
// already-tuned remote-transfer path.
|
|
4030
|
+
//
|
|
4031
|
+
// The one-way-public-UDP blackhole is handled by keeping SENDREQUEST/CONTROL
|
|
4032
|
+
// on relay during negotiation (`forceFileRelay`) until a CONTROL packet
|
|
4033
|
+
// proves the receiver is responding. Once `lastUdpRecvMs` is fresh, bulk may
|
|
4034
|
+
// ride UDP only; if it goes stale, we fall back to relay/TCP.
|
|
4024
4035
|
// Bulk IP data rides the direct path ONLY while it's fresh. When it
|
|
4025
4036
|
// goes stale (likely a NAT remap mid-stream), we do NOT keep firing
|
|
4026
4037
|
// into the dead endpoint and we do NOT duplicate onto UDP — we bridge
|
|
@@ -4030,7 +4041,7 @@ export class Peer {
|
|
|
4030
4041
|
// (to keep the NAT mapping warm) so we still try UDP for them.
|
|
4031
4042
|
const tryUdp = !forceFileRelay &&
|
|
4032
4043
|
realUdpRemote &&
|
|
4033
|
-
(isBulkData ? udpFresh
|
|
4044
|
+
(isBulkData ? udpFresh : true);
|
|
4034
4045
|
if (tryUdp && s?.remote) {
|
|
4035
4046
|
try {
|
|
4036
4047
|
await this.#sendPacket(packet, s.remote);
|
|
@@ -4041,8 +4052,9 @@ export class Peer {
|
|
|
4041
4052
|
}
|
|
4042
4053
|
}
|
|
4043
4054
|
// Fresh confirmed direct path → send over UDP only, but ONLY for bulk.
|
|
4044
|
-
// For files
|
|
4045
|
-
//
|
|
4055
|
+
// For files this includes public UDP-direct, not only LAN; the file layer
|
|
4056
|
+
// handles loss and retransmit. Sparse control/chat still also go over relay
|
|
4057
|
+
// because inbound UDP freshness alone does not prove outbound reachability.
|
|
4046
4058
|
//
|
|
4047
4059
|
// Low-volume traffic (chat 64 + control keepalives / endpoint offers /
|
|
4048
4060
|
// profile) instead ALSO goes over the relay even when the direct path
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.111",
|
|
4
4
|
"description": "Pure TypeScript port of Elastos Carrier (toxcore-derived) P2P messaging. DHT, onion routing, TCP relay, FlatBuffers app payloads, Express offline relay. Wire-compatible with iOS Beagle and the Carrier C SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|