@decentnetwork/peer 0.1.105 → 0.1.107

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
  }
@@ -280,6 +280,8 @@ export class FileTransferManager {
280
280
  btlBwBps: 0, bwSamples: [], fullBwRounds: 0, startupQueueRounds: 0, bbrPhase: 0, roundCount: 0,
281
281
  rateMarkMs: nowMs(), rateMarkAcked: 0, lowRttCount: 0, lowRttMinMs: Infinity, lastCcLogMs: 0,
282
282
  lanPath,
283
+ dbgLanNow: lanPath ? 1 : 0, dbgSampleMs: 0, dbgRoundRate: 0, dbgMeasuredRate: 0,
284
+ dbgRawQueue: 0, dbgUnder: 0, dbgOvershot: 0,
283
285
  };
284
286
  this.#map(this.#sending, friendId).set(fileNumber, st);
285
287
  void this.send(friendId, PACKET_ID_FILE_SENDREQUEST, req).catch(() => undefined);
@@ -330,27 +332,54 @@ export class FileTransferManager {
330
332
  }
331
333
  }
332
334
  cancel(friendId, fileNumber, isSending) {
335
+ const inner = (isSending ? this.#sending : this.#receiving).get(friendId);
336
+ const st = inner?.get(fileNumber);
337
+ const fileId = st ? hex(st.fileId) : undefined;
333
338
  const sr = isSending ? 0 : 1;
334
339
  void this.send(friendId, PACKET_ID_FILE_CONTROL, encodeFileControl(sr, fileNumber, FILECONTROL.KILL)).catch(() => undefined);
335
340
  if (isSending)
336
341
  this.#endSend(friendId, fileNumber);
337
342
  else
338
343
  this.#endRecv(friendId, fileNumber);
344
+ // Local cancel used to tear down silently. The daemon tracks sends in
345
+ // activeSends and needs this event when the registry entry is already gone
346
+ // (or cancel raced with a progress tick) so the UI chip cannot stay "sending".
347
+ if (fileId)
348
+ this.emit("file-cancel", { friendId, fileId, sending: isSending, reason: "local" });
339
349
  }
340
350
  // Cancel by content fileId (hex) — the daemon/UI tracks transfers by fileId,
341
351
  // not the internal fileNumber, so it can stop a specific in-flight transfer.
342
352
  cancelByFileId(friendId, fileIdHex, isSending) {
353
+ const want = fileIdHex.trim().toLowerCase();
343
354
  const inner = (isSending ? this.#sending : this.#receiving).get(friendId);
344
355
  if (!inner)
345
356
  return false;
346
357
  for (const [fileNumber, st] of inner) {
347
- if (hex(st.fileId) === fileIdHex) {
358
+ if (hex(st.fileId) === want) {
348
359
  this.cancel(friendId, fileNumber, isSending);
349
360
  return true;
350
361
  }
351
362
  }
352
363
  return false;
353
364
  }
365
+ /** Cancel in-flight sends matching both name and size.
366
+ * This is a conservative registry-desync fallback after fileId/hash lookup. */
367
+ cancelSendsMatching(friendId, match) {
368
+ const inner = this.#sending.get(friendId);
369
+ if (!inner)
370
+ return [];
371
+ const candidates = [...inner].filter(([, st]) => st.size === match.size);
372
+ const named = match.name
373
+ ? candidates.filter(([, st]) => st.name === match.name)
374
+ : [];
375
+ const picked = named;
376
+ const killed = [];
377
+ for (const [fileNumber, st] of picked) {
378
+ killed.push(hex(st.fileId));
379
+ this.cancel(friendId, fileNumber, true);
380
+ }
381
+ return killed;
382
+ }
354
383
  handlePacket(friendId, packetId, payload) {
355
384
  if (packetId === PACKET_ID_FILE_SENDREQUEST) {
356
385
  this.#onSendRequest(friendId, payload);
@@ -798,6 +827,12 @@ export class FileTransferManager {
798
827
  const measuredRate = lanNow
799
828
  ? Math.min(roundRate, Math.max(PACE_INIT_BPS, st.paceRateBps * LAN_SAMPLE_MAX_GAIN))
800
829
  : roundRate;
830
+ st.dbgLanNow = lanNow ? 1 : 0;
831
+ st.dbgSampleMs = Math.round(sampleMs);
832
+ st.dbgRoundRate = Math.round(roundRate);
833
+ st.dbgMeasuredRate = Math.round(measuredRate);
834
+ st.dbgUnder = 0;
835
+ st.dbgOvershot = 0;
801
836
  if (measuredRate > 0) {
802
837
  st.bwSamples.push(measuredRate);
803
838
  if (st.bwSamples.length > PACE_BW_WINDOW)
@@ -822,6 +857,8 @@ export class FileTransferManager {
822
857
  st.cwnd = Math.max(st.cwnd, LAN_CWND_INIT_CHUNKS);
823
858
  }
824
859
  st.lanPath = lanNow;
860
+ const rawQueue = st.minRttMs !== Infinity ? acceptedRtt - st.minRttMs : 0;
861
+ st.dbgRawQueue = Math.round(rawQueue);
825
862
  if (!lanNow) {
826
863
  // Relay/public-NAT file traffic must remain conservative. A short
827
864
  // ACK burst can wildly overstate bandwidth there; allowing LAN's
@@ -849,7 +886,6 @@ export class FileTransferManager {
849
886
  else {
850
887
  st.fullBwRounds++;
851
888
  }
852
- const rawQueue = st.minRttMs !== Infinity ? acceptedRtt - st.minRttMs : 0;
853
889
  if (rawQueue > LAN_STARTUP_QUEUE_MS) {
854
890
  st.startupQueueRounds++;
855
891
  }
@@ -864,6 +900,8 @@ export class FileTransferManager {
864
900
  const startupOvershot = rawQueue > LAN_STARTUP_HARD_QUEUE_MS ||
865
901
  st.startupQueueRounds >= LAN_STARTUP_QUEUE_ROUNDS;
866
902
  const startupUnderDelivering = st.fullBwRounds >= LAN_STARTUP_SLOW_ROUNDS;
903
+ st.dbgUnder = startupUnderDelivering ? 1 : 0;
904
+ st.dbgOvershot = startupOvershot ? 1 : 0;
867
905
  if (startupUnderDelivering || startupOvershot) {
868
906
  st.bbrPhase = 2;
869
907
  }
@@ -904,15 +942,26 @@ export class FileTransferManager {
904
942
  }
905
943
  if (process.env.DECENT_DEBUG && nowT - st.lastCcLogMs >= 1000) {
906
944
  st.lastCcLogMs = nowT;
945
+ const windowEnd = Math.min(st.acked + st.cwnd * MAX_FILE_DATA_SIZE, st.size);
907
946
  console.error(`[file-cc] friend=${friendId.slice(0, 8)} ack=${st.acked}/${st.size}` +
908
947
  ` rtt=${rtt}ms srtt=${st.srttMs.toFixed(1)}ms low=${st.lowRttCount}` +
909
948
  ` accepted=${(!lowSample || promoteLowPath) ? 1 : 0}` +
910
949
  ` pace=${Math.round(st.paceRateBps)}Bps bw=${Math.round(st.btlBwBps)}Bps` +
911
950
  ` cwnd=${st.cwnd} phase=${st.bbrPhase} slow=${st.fullBwRounds}` +
912
- ` loss=${(st.lossEwma * 100).toFixed(1)}% rounds=${st.roundCount}`);
951
+ ` loss=${(st.lossEwma * 100).toFixed(1)}% rounds=${st.roundCount}` +
952
+ ` lanNow=${st.dbgLanNow} lanPath=${st.lanPath ? 1 : 0}` +
953
+ ` sampleMs=${st.dbgSampleMs} roundRate=${st.dbgRoundRate}` +
954
+ ` measuredRate=${st.dbgMeasuredRate} rawQueue=${st.dbgRawQueue}` +
955
+ ` startupQueueRounds=${st.startupQueueRounds}` +
956
+ ` under=${st.dbgUnder} overshot=${st.dbgOvershot}` +
957
+ ` rateMarkAcked=${st.rateMarkAcked} nextSend=${st.nextSend}` +
958
+ ` windowEnd=${windowEnd} tokens=${Math.round(st.paceTokens)}`);
913
959
  }
914
960
  }
915
- if (st.acked - st.lastProgressAcked >= 256 * 1024 || st.acked >= st.size) {
961
+ // Small files (UI screenshots etc.) used to sit at 0% until the final
962
+ // byte because progress only fired every 256 KB. Emit at least every
963
+ // 16 KB or on completion so a 100–200 KB send shows movement.
964
+ if (st.acked - st.lastProgressAcked >= 16 * 1024 || st.acked >= st.size) {
916
965
  st.lastProgressAcked = st.acked;
917
966
  this.emit("file-progress", { friendId, fileId: hex(st.fileId), received: st.acked, total: st.size, sending: true });
918
967
  }
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.107",
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",