@decentnetwork/peer 0.1.108 → 0.1.110

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,
@@ -821,7 +823,9 @@ export class FileTransferManager {
821
823
  const roundRate = ((st.acked - st.rateMarkAcked) * 1000) / sampleMs;
822
824
  st.rateMarkMs = nowT;
823
825
  st.rateMarkAcked = st.acked;
824
- const lanNow = this.isLanPath(friendId);
826
+ const pathKind = this.pathKind(friendId);
827
+ const lanNow = pathKind === "lan";
828
+ const udpDirectNow = pathKind === "udp-direct";
825
829
  // On a paced LAN flow, sustainable delivery cannot suddenly be
826
830
  // many times faster than the bytes we are putting on the wire.
827
831
  // Such a sample is delayed/coalesced ACKs draining, not new
@@ -861,15 +865,46 @@ export class FileTransferManager {
861
865
  st.lanPath = lanNow;
862
866
  const rawQueue = st.minRttMs !== Infinity ? acceptedRtt - st.minRttMs : 0;
863
867
  st.dbgRawQueue = Math.round(rawQueue);
864
- if (!lanNow) {
865
- // Relay/public-NAT file traffic must remain conservative. A short
868
+ if (!lanNow && !udpDirectNow) {
869
+ // TCP-relay-only file traffic must remain conservative. A short
866
870
  // ACK burst can wildly overstate bandwidth there; allowing LAN's
867
871
  // 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.
872
+ // A confirmed public UDP-direct path is different: it is the old
873
+ // long-haul file path with FEC and BBR-style pacing, so do not
874
+ // pin it to 120-240 KB/s.
869
875
  st.bbrPhase = 2;
870
876
  const gain = PACE_CYCLE_GAINS[st.roundCount % PACE_CYCLE_GAINS.length];
871
877
  st.paceRateBps = Math.max(PACE_INIT_BPS, Math.min(PACE_INIT_BPS * 2, st.btlBwBps * gain * lossMult));
872
878
  }
879
+ else if (udpDirectNow) {
880
+ // Public UDP direct: restore the pre-LAN-regression remote path.
881
+ // It keeps FEC loss compensation and paced BBR startup/probe, but
882
+ // does NOT use LAN's aggressive 2 MB/s initial probe or ACK-burst
883
+ // sample cap. Queue growth still exits STARTUP quickly, so this
884
+ // does not recreate the deep TCP-relay bufferbloat issue.
885
+ if (st.bbrPhase === 0) {
886
+ if (st.fullBwBps === 0 || st.btlBwBps >= st.fullBwBps * 1.20) {
887
+ st.fullBwBps = st.btlBwBps;
888
+ st.fullBwRounds = 0;
889
+ }
890
+ else {
891
+ st.fullBwRounds++;
892
+ }
893
+ if (st.fullBwRounds >= 3 || rawQueue > 350) {
894
+ st.bbrPhase = 2;
895
+ }
896
+ if (st.bbrPhase === 0) {
897
+ st.paceRateBps = Math.max(st.paceRateBps * 1.4, st.btlBwBps * 2.0 * lossMult);
898
+ }
899
+ else {
900
+ st.paceRateBps = Math.max(PACE_INIT_BPS * 0.5, st.btlBwBps * lossMult);
901
+ }
902
+ }
903
+ else {
904
+ const gain = PACE_CYCLE_GAINS[st.roundCount % PACE_CYCLE_GAINS.length];
905
+ st.paceRateBps = Math.max(PACE_INIT_BPS * 0.5, st.btlBwBps * gain * lossMult);
906
+ }
907
+ }
873
908
  else if (st.bbrPhase === 0) {
874
909
  // BBR full-bandwidth detection: remain in STARTUP while delivered
875
910
  // bandwidth grows. A relative RTT threshold is unusable on LAN —
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();
@@ -2169,17 +2179,21 @@ export class Peer {
2169
2179
  // syscall (that was the CCTV CPU regression) — safe at CCTV packet
2170
2180
  // rates.
2171
2181
  this.#adoptRemote(state, remote.address, remote.port);
2172
- // A successfully decrypted packet arriving from a private address in
2173
- // one of our physical subnets is stronger LAN proof than an endpoint
2174
- // hint: it demonstrates that this exact host:port already reaches us.
2175
- // Lock it immediately. Without this, duplicate public/relay copies
2176
- // arriving a moment later overwrite `remote`, so file DATA travels on
2177
- // the LAN but ACKs hairpin through the public Internet (observed as
2178
- // ~38KB/s between mac-dev and WSL on the same 10.0.0.0/24).
2182
+ // A successfully decrypted packet arriving from a private address is
2183
+ // stronger proof than an endpoint hint: it demonstrates that this
2184
+ // exact host:port already reaches us. Do not require the address to be
2185
+ // in one of this process' physical subnets. WSL commonly has only a
2186
+ // 172.16/12 vNIC while the real LAN peer is 10.0.0.x via the Windows
2187
+ // host NAT; direct UDP is confirmed, but a strict same-subnet check
2188
+ // leaves file transfer on the conservative public/relay pacer.
2189
+ //
2190
+ // Unverified private candidates still require same-subnet matching at
2191
+ // candidate-learn time. This relaxed lock is only for decrypted UDP,
2192
+ // so it cannot promote a spoofed or merely advertised private host.
2179
2193
  if (!state.lanRemoteHost &&
2180
2194
  isPrivateAddress(remote.address) &&
2181
2195
  !isCgnatAddress(remote.address) &&
2182
- getPhysicalLanSubnets().some((s) => isInIpv4Subnet(remote.address, s))) {
2196
+ !this.#isUnroutableSelfSource(remote.address, remote.port)) {
2183
2197
  state.lanRemoteHost = remote.address;
2184
2198
  this.#rememberEndpointCandidate(state, remote.address, remote.port);
2185
2199
  this.#debugLog(`lan_lock_confirmed friend=${friendId} host=${remote.address}:${remote.port} (decrypted UDP source)`);
@@ -4008,15 +4022,16 @@ export class Peer {
4008
4022
  Date.now() - s.lastUdpRecvMs < 4_000;
4009
4023
  const forceFileRelay = (this.#fileRelayNegotiationUntil.get(friendId) ?? 0) > Date.now() &&
4010
4024
  !(s?.lanRemoteHost && s.remote?.host === s.lanRemoteHost);
4011
- // A freshly-received UDP packet proves only the peer -> us direction.
4012
- // That is enough for lossy IP/video bulk, whose inner TCP stream can
4013
- // recover a dropped packet, but not for reliable file data: returning
4014
- // UDP-only on a one-way public/NAT endpoint silently black-holes every
4015
- // chunk. A physical-LAN lock is stronger evidence because the candidate
4016
- // was validated against the local subnet; otherwise file bulk continues
4017
- // to TURN/TCP below.
4018
- const fileUdpOnlyConfirmed = !reliableOnRelay ||
4019
- (!!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.
4020
4035
  // Bulk IP data rides the direct path ONLY while it's fresh. When it
4021
4036
  // goes stale (likely a NAT remap mid-stream), we do NOT keep firing
4022
4037
  // into the dead endpoint and we do NOT duplicate onto UDP — we bridge
@@ -4026,7 +4041,7 @@ export class Peer {
4026
4041
  // (to keep the NAT mapping warm) so we still try UDP for them.
4027
4042
  const tryUdp = !forceFileRelay &&
4028
4043
  realUdpRemote &&
4029
- (isBulkData ? udpFresh && fileUdpOnlyConfirmed : true);
4044
+ (isBulkData ? udpFresh : true);
4030
4045
  if (tryUdp && s?.remote) {
4031
4046
  try {
4032
4047
  await this.#sendPacket(packet, s.remote);
@@ -4037,8 +4052,9 @@ export class Peer {
4037
4052
  }
4038
4053
  }
4039
4054
  // Fresh confirmed direct path → send over UDP only, but ONLY for bulk.
4040
- // For files, `tryUdp` above requires the physical-LAN lock; a merely-fresh
4041
- // 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.
4042
4058
  //
4043
4059
  // Low-volume traffic (chat 64 + control keepalives / endpoint offers /
4044
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.108",
3
+ "version": "0.1.110",
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",