@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.
@@ -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
- constructor(send: FtSend, emit: FtEmit, isLanPath?: FtIsLanPath);
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 (most importantly tcp-relay LAN UDP). The
789
- // old code rejected every such sample against the permanently-high srtt,
790
- // so startup stayed pinned near 120 KB/s forever. Promote a stable lower
791
- // RTT after three samples and rebase srtt/RTprop to the new path.
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 lanNow = this.isLanPath(friendId);
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
- // Relay/public-NAT file traffic must remain conservative. A short
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
- // This affects only file DATA/FEC IP/video has its own path.
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
- // A freshly-received UDP packet proves only the peer -> us direction.
4016
- // That is enough for lossy IP/video bulk, whose inner TCP stream can
4017
- // recover a dropped packet, but not for reliable file data: returning
4018
- // UDP-only on a one-way public/NAT endpoint silently black-holes every
4019
- // chunk. A physical-LAN lock is stronger evidence because the candidate
4020
- // was validated against the local subnet; otherwise file bulk continues
4021
- // to TURN/TCP below.
4022
- const fileUdpOnlyConfirmed = !reliableOnRelay ||
4023
- (!!s?.lanRemoteHost && s.remote?.host === s.lanRemoteHost);
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 && fileUdpOnlyConfirmed : true);
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, `tryUdp` above requires the physical-LAN lock; a merely-fresh
4045
- // public endpoint deliberately falls through to reliable TURN/TCP.
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.109",
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",