@decentnetwork/peer 0.1.105 → 0.1.108

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.
@@ -57,6 +57,12 @@ export declare class FileTransferManager {
57
57
  accept(friendId: string, fileNumber: number): void;
58
58
  cancel(friendId: string, fileNumber: number, isSending: boolean): void;
59
59
  cancelByFileId(friendId: string, fileIdHex: string, isSending: boolean): boolean;
60
+ /** Cancel in-flight sends matching both name and size.
61
+ * This is a conservative registry-desync fallback after fileId/hash lookup. */
62
+ cancelSendsMatching(friendId: string, match: {
63
+ name?: string;
64
+ size: number;
65
+ }): string[];
60
66
  handlePacket(friendId: string, packetId: number, payload: Uint8Array): boolean;
61
67
  clearFriend(friendId: string): void;
62
68
  }
@@ -119,6 +119,8 @@ const LAN_STARTUP_QUEUE_MS = 500; // unmistakable raw queue spike; smaller app-l
119
119
  const LAN_STARTUP_HARD_QUEUE_MS = 2000; // one multi-second queue sample is unsafe
120
120
  const LAN_STARTUP_QUEUE_ROUNDS = 2; // tolerate one scheduler/session-handover outlier on LAN
121
121
  const LAN_STARTUP_KEEPUP = 0.70; // delivered goodput below 70% of pace means the receiver is no longer keeping up
122
+ const LAN_RECOVERY_GAIN = 1.20; // after a LAN overshoot drains, climb out of the 120KB/s floor quickly but smoothly
123
+ const LAN_RECOVERY_QUEUE_MS = 250; // only recover while queueing is clearly gone
122
124
  // Five 200 ms samples was still only about one Windows ACK scheduling cycle in
123
125
  // practice: a clean LAN (0% loss) exited STARTUP after ~3 MB and threw away a
124
126
  // healthy 2.3 MB/s probe. Require a few seconds of sustained under-delivery;
@@ -280,6 +282,8 @@ export class FileTransferManager {
280
282
  btlBwBps: 0, bwSamples: [], fullBwRounds: 0, startupQueueRounds: 0, bbrPhase: 0, roundCount: 0,
281
283
  rateMarkMs: nowMs(), rateMarkAcked: 0, lowRttCount: 0, lowRttMinMs: Infinity, lastCcLogMs: 0,
282
284
  lanPath,
285
+ dbgLanNow: lanPath ? 1 : 0, dbgSampleMs: 0, dbgRoundRate: 0, dbgMeasuredRate: 0,
286
+ dbgRawQueue: 0, dbgUnder: 0, dbgOvershot: 0,
283
287
  };
284
288
  this.#map(this.#sending, friendId).set(fileNumber, st);
285
289
  void this.send(friendId, PACKET_ID_FILE_SENDREQUEST, req).catch(() => undefined);
@@ -330,27 +334,54 @@ export class FileTransferManager {
330
334
  }
331
335
  }
332
336
  cancel(friendId, fileNumber, isSending) {
337
+ const inner = (isSending ? this.#sending : this.#receiving).get(friendId);
338
+ const st = inner?.get(fileNumber);
339
+ const fileId = st ? hex(st.fileId) : undefined;
333
340
  const sr = isSending ? 0 : 1;
334
341
  void this.send(friendId, PACKET_ID_FILE_CONTROL, encodeFileControl(sr, fileNumber, FILECONTROL.KILL)).catch(() => undefined);
335
342
  if (isSending)
336
343
  this.#endSend(friendId, fileNumber);
337
344
  else
338
345
  this.#endRecv(friendId, fileNumber);
346
+ // Local cancel used to tear down silently. The daemon tracks sends in
347
+ // activeSends and needs this event when the registry entry is already gone
348
+ // (or cancel raced with a progress tick) so the UI chip cannot stay "sending".
349
+ if (fileId)
350
+ this.emit("file-cancel", { friendId, fileId, sending: isSending, reason: "local" });
339
351
  }
340
352
  // Cancel by content fileId (hex) — the daemon/UI tracks transfers by fileId,
341
353
  // not the internal fileNumber, so it can stop a specific in-flight transfer.
342
354
  cancelByFileId(friendId, fileIdHex, isSending) {
355
+ const want = fileIdHex.trim().toLowerCase();
343
356
  const inner = (isSending ? this.#sending : this.#receiving).get(friendId);
344
357
  if (!inner)
345
358
  return false;
346
359
  for (const [fileNumber, st] of inner) {
347
- if (hex(st.fileId) === fileIdHex) {
360
+ if (hex(st.fileId) === want) {
348
361
  this.cancel(friendId, fileNumber, isSending);
349
362
  return true;
350
363
  }
351
364
  }
352
365
  return false;
353
366
  }
367
+ /** Cancel in-flight sends matching both name and size.
368
+ * This is a conservative registry-desync fallback after fileId/hash lookup. */
369
+ cancelSendsMatching(friendId, match) {
370
+ const inner = this.#sending.get(friendId);
371
+ if (!inner)
372
+ return [];
373
+ const candidates = [...inner].filter(([, st]) => st.size === match.size);
374
+ const named = match.name
375
+ ? candidates.filter(([, st]) => st.name === match.name)
376
+ : [];
377
+ const picked = named;
378
+ const killed = [];
379
+ for (const [fileNumber, st] of picked) {
380
+ killed.push(hex(st.fileId));
381
+ this.cancel(friendId, fileNumber, true);
382
+ }
383
+ return killed;
384
+ }
354
385
  handlePacket(friendId, packetId, payload) {
355
386
  if (packetId === PACKET_ID_FILE_SENDREQUEST) {
356
387
  this.#onSendRequest(friendId, payload);
@@ -798,6 +829,12 @@ export class FileTransferManager {
798
829
  const measuredRate = lanNow
799
830
  ? Math.min(roundRate, Math.max(PACE_INIT_BPS, st.paceRateBps * LAN_SAMPLE_MAX_GAIN))
800
831
  : roundRate;
832
+ st.dbgLanNow = lanNow ? 1 : 0;
833
+ st.dbgSampleMs = Math.round(sampleMs);
834
+ st.dbgRoundRate = Math.round(roundRate);
835
+ st.dbgMeasuredRate = Math.round(measuredRate);
836
+ st.dbgUnder = 0;
837
+ st.dbgOvershot = 0;
801
838
  if (measuredRate > 0) {
802
839
  st.bwSamples.push(measuredRate);
803
840
  if (st.bwSamples.length > PACE_BW_WINDOW)
@@ -822,6 +859,8 @@ export class FileTransferManager {
822
859
  st.cwnd = Math.max(st.cwnd, LAN_CWND_INIT_CHUNKS);
823
860
  }
824
861
  st.lanPath = lanNow;
862
+ const rawQueue = st.minRttMs !== Infinity ? acceptedRtt - st.minRttMs : 0;
863
+ st.dbgRawQueue = Math.round(rawQueue);
825
864
  if (!lanNow) {
826
865
  // Relay/public-NAT file traffic must remain conservative. A short
827
866
  // ACK burst can wildly overstate bandwidth there; allowing LAN's
@@ -849,7 +888,6 @@ export class FileTransferManager {
849
888
  else {
850
889
  st.fullBwRounds++;
851
890
  }
852
- const rawQueue = st.minRttMs !== Infinity ? acceptedRtt - st.minRttMs : 0;
853
891
  if (rawQueue > LAN_STARTUP_QUEUE_MS) {
854
892
  st.startupQueueRounds++;
855
893
  }
@@ -864,6 +902,8 @@ export class FileTransferManager {
864
902
  const startupOvershot = rawQueue > LAN_STARTUP_HARD_QUEUE_MS ||
865
903
  st.startupQueueRounds >= LAN_STARTUP_QUEUE_ROUNDS;
866
904
  const startupUnderDelivering = st.fullBwRounds >= LAN_STARTUP_SLOW_ROUNDS;
905
+ st.dbgUnder = startupUnderDelivering ? 1 : 0;
906
+ st.dbgOvershot = startupOvershot ? 1 : 0;
867
907
  if (startupUnderDelivering || startupOvershot) {
868
908
  st.bbrPhase = 2;
869
909
  }
@@ -885,8 +925,18 @@ export class FileTransferManager {
885
925
  }
886
926
  else {
887
927
  // PROBE_BW cruise: one sample probes up, one drains, six cruise.
928
+ // If LAN STARTUP hit one real overshoot (e.g. a 1s ACK pause with
929
+ // high reported loss), the normal max-filter can be rebuilt from
930
+ // tiny post-drain samples and then takes tens of seconds to climb
931
+ // out of the 120KB/s floor. Once loss and queueing are gone, let
932
+ // a validated LAN recover toward its safe initial probe rate. The
933
+ // existing loss/queue brakes still stop a bad path immediately.
888
934
  const gain = PACE_CYCLE_GAINS[st.roundCount % PACE_CYCLE_GAINS.length];
889
- st.paceRateBps = Math.max(PACE_INIT_BPS * 0.5, st.btlBwBps * gain * lossMult);
935
+ const cruiseRate = Math.max(PACE_INIT_BPS * 0.5, st.btlBwBps * gain * lossMult);
936
+ const healthyLan = lanNow && st.lossEwma < 0.02 && rawQueue < LAN_RECOVERY_QUEUE_MS;
937
+ st.paceRateBps = healthyLan
938
+ ? Math.max(cruiseRate, Math.min(LAN_PACE_INIT_BPS, Math.max(st.paceRateBps * LAN_RECOVERY_GAIN, measuredRate * LAN_STARTUP_DELIVERY_GAIN)))
939
+ : cruiseRate;
890
940
  }
891
941
  // Hard safety cap: never pace faster than a full window drains in 1 RTT.
892
942
  if (st.minRttMs !== Infinity && st.minRttMs > 0) {
@@ -904,15 +954,26 @@ export class FileTransferManager {
904
954
  }
905
955
  if (process.env.DECENT_DEBUG && nowT - st.lastCcLogMs >= 1000) {
906
956
  st.lastCcLogMs = nowT;
957
+ const windowEnd = Math.min(st.acked + st.cwnd * MAX_FILE_DATA_SIZE, st.size);
907
958
  console.error(`[file-cc] friend=${friendId.slice(0, 8)} ack=${st.acked}/${st.size}` +
908
959
  ` rtt=${rtt}ms srtt=${st.srttMs.toFixed(1)}ms low=${st.lowRttCount}` +
909
960
  ` accepted=${(!lowSample || promoteLowPath) ? 1 : 0}` +
910
961
  ` pace=${Math.round(st.paceRateBps)}Bps bw=${Math.round(st.btlBwBps)}Bps` +
911
962
  ` cwnd=${st.cwnd} phase=${st.bbrPhase} slow=${st.fullBwRounds}` +
912
- ` loss=${(st.lossEwma * 100).toFixed(1)}% rounds=${st.roundCount}`);
963
+ ` loss=${(st.lossEwma * 100).toFixed(1)}% rounds=${st.roundCount}` +
964
+ ` lanNow=${st.dbgLanNow} lanPath=${st.lanPath ? 1 : 0}` +
965
+ ` sampleMs=${st.dbgSampleMs} roundRate=${st.dbgRoundRate}` +
966
+ ` measuredRate=${st.dbgMeasuredRate} rawQueue=${st.dbgRawQueue}` +
967
+ ` startupQueueRounds=${st.startupQueueRounds}` +
968
+ ` under=${st.dbgUnder} overshot=${st.dbgOvershot}` +
969
+ ` rateMarkAcked=${st.rateMarkAcked} nextSend=${st.nextSend}` +
970
+ ` windowEnd=${windowEnd} tokens=${Math.round(st.paceTokens)}`);
913
971
  }
914
972
  }
915
- if (st.acked - st.lastProgressAcked >= 256 * 1024 || st.acked >= st.size) {
973
+ // Small files (UI screenshots etc.) used to sit at 0% until the final
974
+ // byte because progress only fired every 256 KB. Emit at least every
975
+ // 16 KB or on completion so a 100–200 KB send shows movement.
976
+ if (st.acked - st.lastProgressAcked >= 16 * 1024 || st.acked >= st.size) {
916
977
  st.lastProgressAcked = st.acked;
917
978
  this.emit("file-progress", { friendId, fileId: hex(st.fileId), received: st.acked, total: st.size, sending: true });
918
979
  }
package/dist/peer.d.ts CHANGED
@@ -164,6 +164,11 @@ export declare class Peer {
164
164
  cancelFile(userid: string, fileNumber: number, isSending?: boolean): void;
165
165
  /** Cancel an in-flight transfer by its content fileId (hex). Returns true if found. */
166
166
  cancelFileById(userid: string, fileId: string, isSending?: boolean): boolean;
167
+ /** Cancel in-flight sends that match size/name (registry-desync fallback). */
168
+ cancelSendsMatching(userid: string, match: {
169
+ name?: string;
170
+ size: number;
171
+ }): string[];
167
172
  /** Incoming file offer: { friendId, fileNumber, fileId, name, size, kind }. */
168
173
  onFile(cb: (offer: {
169
174
  friendId: string;
package/dist/peer.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { EventEmitter } from "node:events";
2
+ import { execFileSync } from "node:child_process";
3
+ import { existsSync } from "node:fs";
2
4
  import { mkdir, readFile, writeFile, rename } from "node:fs/promises";
3
5
  import { networkInterfaces } from "node:os";
4
6
  import { dirname, join } from "node:path";
@@ -1348,6 +1350,10 @@ export class Peer {
1348
1350
  cancelFile(userid, fileNumber, isSending = false) { this.#fileTransfer.cancel(userid, fileNumber, isSending); }
1349
1351
  /** Cancel an in-flight transfer by its content fileId (hex). Returns true if found. */
1350
1352
  cancelFileById(userid, fileId, isSending = false) { return this.#fileTransfer.cancelByFileId(userid, fileId, isSending); }
1353
+ /** Cancel in-flight sends that match size/name (registry-desync fallback). */
1354
+ cancelSendsMatching(userid, match) {
1355
+ return this.#fileTransfer.cancelSendsMatching(userid, match);
1356
+ }
1351
1357
  /** Incoming file offer: { friendId, fileNumber, fileId, name, size, kind }. */
1352
1358
  onFile(cb) { this.#events.on("file-offer", cb); }
1353
1359
  /** Transfer progress: { friendId, fileId, received, total, sending? }. */
@@ -4489,7 +4495,11 @@ export class Peer {
4489
4495
  // office silently fall back to the TURN relay (~260ms for a <1ms hop).
4490
4496
  const lanIp = getPhysicalLanAddresses().find((ip) => isPrivateAddress(ip));
4491
4497
  const lanOctets = lanIp ? lanIp.split(".").map((s) => parseInt(s, 10)) : undefined;
4492
- const lanPort = this.#udp.localPort() ?? 0;
4498
+ // WSL packets are NATed by Windows, so the physical host NIC must be paired
4499
+ // with the observed server-reflexive port, not WSL's internal socket port.
4500
+ const lanPort = lanIp && _wslHostAddrsCache.has(lanIp)
4501
+ ? srflx.port
4502
+ : (this.#udp.localPort() ?? 0);
4493
4503
  const lanValid = !!lanOctets && lanOctets.length === 4 && !lanOctets.some((o) => Number.isNaN(o)) && lanPort > 0;
4494
4504
  const size = lanValid ? 18 : relayValid ? 12 : 6;
4495
4505
  const payload = new Uint8Array(size);
@@ -6327,6 +6337,45 @@ let _lanAddrsCache = [];
6327
6337
  let _lanSubnetsCache = [];
6328
6338
  let _allOwnAddrsCache = new Set();
6329
6339
  let _ownVirtualAddrsCache = new Set();
6340
+ let _wslHostAddrsCache = new Set();
6341
+ let _wslHostCacheMs = 0;
6342
+ /** Discover the Windows host's physical NIC from WSL2. WSL itself only sees
6343
+ * its 172.x Hyper-V NIC, which is unreachable from another LAN machine. */
6344
+ function getWslWindowsHostAddresses(localSubnets) {
6345
+ const now = Date.now();
6346
+ if (now - _wslHostCacheMs < 60_000)
6347
+ return [..._wslHostAddrsCache];
6348
+ _wslHostCacheMs = now;
6349
+ const found = new Set();
6350
+ const ipconfig = "/mnt/c/Windows/System32/ipconfig.exe";
6351
+ if (process.platform !== "linux" || !existsSync(ipconfig)) {
6352
+ _wslHostAddrsCache = found;
6353
+ return [];
6354
+ }
6355
+ try {
6356
+ const output = execFileSync(ipconfig, [], {
6357
+ encoding: "utf8",
6358
+ timeout: 2000,
6359
+ windowsHide: true,
6360
+ stdio: ["ignore", "pipe", "ignore"]
6361
+ });
6362
+ // "IPv4" remains in localized ipconfig output (including Chinese).
6363
+ for (const match of output.matchAll(/IPv4[^:\r\n]*:\s*(\d+\.\d+\.\d+\.\d+)/gi)) {
6364
+ const host = match[1];
6365
+ if (!isPrivateAddress(host) || isCgnatAddress(host) || host.startsWith("127.") || host.startsWith("169.254."))
6366
+ continue;
6367
+ // Exclude WSL/Hyper-V addresses already visible inside Linux.
6368
+ if (localSubnets.some((s) => isInIpv4Subnet(host, s)))
6369
+ continue;
6370
+ found.add(host);
6371
+ }
6372
+ }
6373
+ catch {
6374
+ // WSL interop can be disabled; ordinary interface discovery remains valid.
6375
+ }
6376
+ _wslHostAddrsCache = found;
6377
+ return [...found];
6378
+ }
6330
6379
  function refreshLanIfaceCache() {
6331
6380
  const now = Date.now();
6332
6381
  if (_lanIfaceCacheMs !== 0 && now - _lanIfaceCacheMs < 5000)
@@ -6363,6 +6412,11 @@ function refreshLanIfaceCache() {
6363
6412
  catch {
6364
6413
  // best-effort
6365
6414
  }
6415
+ // Prefer the physical Windows host NIC over WSL's internal eth0 address.
6416
+ const wslHostAddrs = getWslWindowsHostAddresses(subnets);
6417
+ for (const host of wslHostAddrs)
6418
+ allOwn.add(host);
6419
+ addrs.unshift(...wslHostAddrs);
6366
6420
  _lanAddrsCache = addrs;
6367
6421
  _lanSubnetsCache = subnets;
6368
6422
  _allOwnAddrsCache = allOwn;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/peer",
3
- "version": "0.1.105",
3
+ "version": "0.1.108",
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",