@decentnetwork/peer 0.1.101 → 0.1.104

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.
@@ -96,6 +96,14 @@ const PACE_CYCLE_GAINS = [1.25, 0.8, 1, 1, 1, 1, 1, 1]; // steady PROBE_BW cycle
96
96
  const PACE_BW_WINDOW = 10; // rounds of delivered-rate history for the max-filter
97
97
  const PACE_SAMPLE_MS = 200; // aggregate ACKs so one scheduler-jittered packet is not a "round"
98
98
  const PACE_INIT_BPS = 120_000; // initial pace rate (~120 KB/s) before any bandwidth estimate
99
+ // A subnet-validated endpoint is not an unknown Internet/relay path. Starting
100
+ // it at 120 KB/s made a Windows ACK-scheduling wobble terminate STARTUP before
101
+ // the controller had ever offered meaningful LAN load; transfers then stayed
102
+ // around 200-500 KB/s despite a multi-MB/s physical path. 2 MB/s is a starting
103
+ // probe, not a cap: the normal feedback loop can grow above it or immediately
104
+ // drain below it on real loss/queueing. The 20 ms pacer limits the initial burst
105
+ // to ~40 KB, so this does not recreate the old 1 MB window blast.
106
+ const LAN_PACE_INIT_BPS = 2_000_000;
99
107
  const PACE_BURST_MS = 20; // token-bucket burst allowance (keeps sends smooth, not bunched)
100
108
  // A LAN receiver can coalesce a backlog of ACKs after its event loop wakes. The
101
109
  // resulting delivery sample is ACK rate, not link capacity: in practice a
@@ -111,7 +119,11 @@ const LAN_STARTUP_QUEUE_MS = 500; // unmistakable raw queue spike; smaller app-l
111
119
  const LAN_STARTUP_HARD_QUEUE_MS = 2000; // one multi-second queue sample is unsafe
112
120
  const LAN_STARTUP_QUEUE_ROUNDS = 2; // tolerate one scheduler/session-handover outlier on LAN
113
121
  const LAN_STARTUP_KEEPUP = 0.70; // delivered goodput below 70% of pace means the receiver is no longer keeping up
114
- const LAN_STARTUP_SLOW_ROUNDS = 5; // require sustained saturation across Windows scheduler/ACK jitter
122
+ // Five 200 ms samples was still only about one Windows ACK scheduling cycle in
123
+ // practice: a clean LAN (0% loss) exited STARTUP after ~3 MB and threw away a
124
+ // healthy 2.3 MB/s probe. Require a few seconds of sustained under-delivery;
125
+ // the independent queue/loss brakes still react immediately to a real overload.
126
+ const LAN_STARTUP_SLOW_ROUNDS = 15;
115
127
  // FILE ACKs are coalesced and handled by the remote JS event loop. On Windows
116
128
  // the effective ACK clock is commonly 20–120 ms even when an occasional probe
117
129
  // reports a 7 ms network RTT. Sizing cwnd from that lucky minimum made a
@@ -119,6 +131,7 @@ const LAN_STARTUP_SLOW_ROUNDS = 5; // require sustained saturation across Window
119
131
  // and falsely ended startup. Pacing remains the primary throttle, so this floor
120
132
  // only prevents the safety window from starving a validated LAN flow.
121
133
  const LAN_BDP_RTT_FLOOR_MS = 50;
134
+ const LAN_CWND_INIT_CHUNKS = Math.ceil((LAN_PACE_INIT_BPS * (LAN_BDP_RTT_FLOOR_MS / 1000) * 2) / MAX_FILE_DATA_SIZE);
122
135
  const WATCHDOG_MS = 150; // stall poll cadence / base no-progress patience
123
136
  const WATCHDOG_MAX_MS = 8000; // cap the RTT-adaptive stall patience (must exceed a slow path's RTT so acks aren't mistaken for a stall)
124
137
  const FAST_RT_MIN_MS = 40; // min gap between fast-retransmits of the same hole (≈1 RTT)
@@ -245,16 +258,19 @@ export class FileTransferManager {
245
258
  ? new Uint8Array(createHash("sha256").update(data).digest()).slice(0, FILE_ID_LENGTH)
246
259
  : randomBytes(FILE_ID_LENGTH);
247
260
  const req = encodeFileSendRequest(fileNumber, opts.kind ?? FILEKIND.DATA, BigInt(data.length), fileId, opts.name);
261
+ const lanPath = this.isLanPath(friendId);
248
262
  const st = {
249
263
  fileNumber, fileId, name: opts.name, data, size: data.length,
250
264
  acked: 0, nextSend: 0, pumping: false, started: false, req, startMs: nowMs(),
251
265
  lastProgressAcked: 0, lastAckAdvanceMs: nowMs(), lastResendMs: 0,
252
- cwnd: CWND_INIT_CHUNKS, stalls: 0, parityHighBlock: -1, lastLossMs: 0, lossEwma: 0,
266
+ cwnd: lanPath ? LAN_CWND_INIT_CHUNKS : CWND_INIT_CHUNKS,
267
+ stalls: 0, parityHighBlock: -1, lastLossMs: 0, lossEwma: 0,
253
268
  minRttMs: Infinity, srttMs: 0, probeOffset: -1, probeSentMs: 0,
254
- paceRateBps: PACE_INIT_BPS, paceTokens: 0, lastPaceMs: nowMs(),
269
+ paceRateBps: lanPath ? LAN_PACE_INIT_BPS : PACE_INIT_BPS,
270
+ paceTokens: 0, lastPaceMs: nowMs(),
255
271
  btlBwBps: 0, bwSamples: [], fullBwRounds: 0, startupQueueRounds: 0, bbrPhase: 0, roundCount: 0,
256
272
  rateMarkMs: nowMs(), rateMarkAcked: 0, lowRttCount: 0, lowRttMinMs: Infinity, lastCcLogMs: 0,
257
- lanPath: this.isLanPath(friendId),
273
+ lanPath,
258
274
  };
259
275
  this.#map(this.#sending, friendId).set(fileNumber, st);
260
276
  void this.send(friendId, PACKET_ID_FILE_SENDREQUEST, req).catch(() => undefined);
@@ -786,10 +802,15 @@ export class FileTransferManager {
786
802
  const lossMult = lanNow ? 1 : 1 / (1 - Math.min(0.5, st.lossEwma));
787
803
  if (lanNow && !st.lanPath) {
788
804
  // A transfer can begin on relay and then promote to a validated
789
- // physical-LAN endpoint. Restart STARTUP from the measured rate.
805
+ // physical-LAN endpoint. Relay samples must not pin the new LAN
806
+ // path near 120 KB/s: discard them and begin a safe LAN probe.
790
807
  st.bbrPhase = 0;
791
808
  st.fullBwRounds = 0;
792
809
  st.startupQueueRounds = 0;
810
+ st.bwSamples = [];
811
+ st.btlBwBps = 0;
812
+ st.paceRateBps = Math.max(st.paceRateBps, LAN_PACE_INIT_BPS);
813
+ st.cwnd = Math.max(st.cwnd, LAN_CWND_INIT_CHUNKS);
793
814
  }
794
815
  st.lanPath = lanNow;
795
816
  if (!lanNow) {
package/dist/peer.js CHANGED
@@ -210,8 +210,22 @@ const RECV_REQUEST_MIN_INTERVAL_MS = 200;
210
210
  export class Peer {
211
211
  #opts;
212
212
  #events = new EventEmitter();
213
+ /** While negotiating a non-LAN file, temporarily stop sending this friend
214
+ * UDP control traffic. Peer <=0.1.94 routes FILE_CONTROL UDP-only whenever
215
+ * it has seen recent inbound UDP, even when that public/NAT path is one-way.
216
+ * Repeated relay-only offers let that 4s freshness expire, after which the
217
+ * old peer returns ACCEPT/ACK over its reliable TCP relay. */
218
+ #fileRelayNegotiationUntil = new Map();
213
219
  // Toxcore-standard file transfer (wire-compatible with native Carrier/toxcore).
214
- #fileTransfer = new FileTransferManager((friendId, packetId, payload) => this.#sendMessengerPacket(friendId, packetId, payload), (event, payload) => { this.#events.emit(event, payload); }, (friendId) => {
220
+ #fileTransfer = new FileTransferManager((friendId, packetId, payload) => {
221
+ if (packetId === PACKET_ID_FILE_SENDREQUEST) {
222
+ const session = this.#friendSessions.get(friendId);
223
+ const lan = !!session?.lanRemoteHost && session.remote?.host === session.lanRemoteHost;
224
+ if (!lan)
225
+ this.#fileRelayNegotiationUntil.set(friendId, Date.now() + 6_000);
226
+ }
227
+ return this.#sendMessengerPacket(friendId, packetId, payload);
228
+ }, (event, payload) => { this.#events.emit(event, payload); }, (friendId) => {
215
229
  const session = this.#friendSessions.get(friendId);
216
230
  return !!session?.lanRemoteHost && session.remote?.host === session.lanRemoteHost;
217
231
  });
@@ -604,6 +618,7 @@ export class Peer {
604
618
  clearTimeout(timer);
605
619
  this.#profileRetryTimers.clear();
606
620
  this.#profileRetryAttempts.clear();
621
+ this.#fileRelayNegotiationUntil.clear();
607
622
  if (this.#expressPollTimer) {
608
623
  clearInterval(this.#expressPollTimer);
609
624
  this.#expressPollTimer = undefined;
@@ -980,6 +995,7 @@ export class Peer {
980
995
  }
981
996
  const existed = this.#friends.delete(friendId);
982
997
  this.#friendSessions.delete(friendId);
998
+ this.#fileRelayNegotiationUntil.delete(friendId);
983
999
  this.#pendingFriendRequests.delete(friendId);
984
1000
  this.#cookieRetryCount.delete(friendId);
985
1001
  this.#lastCookieSentKey.delete(friendId);
@@ -3345,6 +3361,11 @@ export class Peer {
3345
3361
  * buffered packets through the exact same dispatch. `kind` is the first
3346
3362
  * payload byte (never undefined here — the caller defaults it). */
3347
3363
  #deliverLosslessPayload(friendId, state, kind, inner, remote, packetNumber = 0) {
3364
+ // Any file response proves the receiver can hear this session. Release the
3365
+ // old-peer relay-only negotiation guard before the data pump chooses its
3366
+ // normal LAN/reliable-public path.
3367
+ if (kind === PACKET_ID_FILE_CONTROL)
3368
+ this.#fileRelayNegotiationUntil.delete(friendId);
3348
3369
  // Toxcore file transfer (FILE_SENDREQUEST/CONTROL/DATA = 80/81/82).
3349
3370
  if (this.#fileTransfer.handlePacket(friendId, kind, inner))
3350
3371
  return;
@@ -3964,6 +3985,8 @@ export class Peer {
3964
3985
  const udpFresh = !!realUdpRemote &&
3965
3986
  s?.lastUdpRecvMs !== undefined &&
3966
3987
  Date.now() - s.lastUdpRecvMs < 4_000;
3988
+ const forceFileRelay = (this.#fileRelayNegotiationUntil.get(friendId) ?? 0) > Date.now() &&
3989
+ !(s?.lanRemoteHost && s.remote?.host === s.lanRemoteHost);
3967
3990
  // A freshly-received UDP packet proves only the peer -> us direction.
3968
3991
  // That is enough for lossy IP/video bulk, whose inner TCP stream can
3969
3992
  // recover a dropped packet, but not for reliable file data: returning
@@ -3980,7 +4003,8 @@ export class Peer {
3980
4003
  // lastUdpRecvMs). This converts a multi-second blackout into a few
3981
4004
  // seconds of higher relay latency. Control packets always probe UDP
3982
4005
  // (to keep the NAT mapping warm) so we still try UDP for them.
3983
- const tryUdp = realUdpRemote &&
4006
+ const tryUdp = !forceFileRelay &&
4007
+ realUdpRemote &&
3984
4008
  (isBulkData ? udpFresh && fileUdpOnlyConfirmed : true);
3985
4009
  if (tryUdp && s?.remote) {
3986
4010
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/peer",
3
- "version": "0.1.101",
3
+ "version": "0.1.104",
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",