@decentnetwork/peer 0.1.104 → 0.1.105

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.
@@ -239,17 +239,23 @@ export class FileTransferManager {
239
239
  }
240
240
  return inner;
241
241
  }
242
- #freeNumber(friendId) {
242
+ #freeNumber(friendId, preferred = 0) {
243
243
  const inner = this.#map(this.#sending, friendId);
244
- for (let i = 0; i < MAX_CONCURRENT_FILE_PIPES; i++)
244
+ // Start at a content-derived slot. Older receivers retain an incomplete
245
+ // receive slot in memory until it finishes or is killed. After the sender
246
+ // restarts, always reusing slot 0 makes a DIFFERENT new file look like a
247
+ // retransmitted offer for that stale slot, so the receiver silently ACKs
248
+ // the old file and the new UI stays at 0%. The same content keeps the same
249
+ // preferred slot (resume), while different content naturally avoids stale
250
+ // collisions; circular search still supports concurrent transfers.
251
+ for (let n = 0; n < MAX_CONCURRENT_FILE_PIPES; n++) {
252
+ const i = (preferred + n) % MAX_CONCURRENT_FILE_PIPES;
245
253
  if (!inner.has(i))
246
254
  return i;
255
+ }
247
256
  return -1;
248
257
  }
249
258
  sendFile(friendId, data, opts) {
250
- const fileNumber = this.#freeNumber(friendId);
251
- if (fileNumber < 0)
252
- return null;
253
259
  // Content-addressed fileId (sha256): re-sending the SAME file after a
254
260
  // restart or a dropped peer yields the SAME id, so the receiver can match
255
261
  // it against its persisted partial and resume from where it left off — the
@@ -257,6 +263,9 @@ export class FileTransferManager {
257
263
  const fileId = data.length > 0
258
264
  ? new Uint8Array(createHash("sha256").update(data).digest()).slice(0, FILE_ID_LENGTH)
259
265
  : randomBytes(FILE_ID_LENGTH);
266
+ const fileNumber = this.#freeNumber(friendId, fileId[0] ?? 0);
267
+ if (fileNumber < 0)
268
+ return null;
260
269
  const req = encodeFileSendRequest(fileNumber, opts.kind ?? FILEKIND.DATA, BigInt(data.length), fileId, opts.name);
261
270
  const lanPath = this.isLanPath(friendId);
262
271
  const st = {
package/dist/peer.js CHANGED
@@ -2163,6 +2163,21 @@ export class Peer {
2163
2163
  // syscall (that was the CCTV CPU regression) — safe at CCTV packet
2164
2164
  // rates.
2165
2165
  this.#adoptRemote(state, remote.address, remote.port);
2166
+ // A successfully decrypted packet arriving from a private address in
2167
+ // one of our physical subnets is stronger LAN proof than an endpoint
2168
+ // hint: it demonstrates that this exact host:port already reaches us.
2169
+ // Lock it immediately. Without this, duplicate public/relay copies
2170
+ // arriving a moment later overwrite `remote`, so file DATA travels on
2171
+ // the LAN but ACKs hairpin through the public Internet (observed as
2172
+ // ~38KB/s between mac-dev and WSL on the same 10.0.0.0/24).
2173
+ if (!state.lanRemoteHost &&
2174
+ isPrivateAddress(remote.address) &&
2175
+ !isCgnatAddress(remote.address) &&
2176
+ getPhysicalLanSubnets().some((s) => isInIpv4Subnet(remote.address, s))) {
2177
+ state.lanRemoteHost = remote.address;
2178
+ this.#rememberEndpointCandidate(state, remote.address, remote.port);
2179
+ this.#debugLog(`lan_lock_confirmed friend=${friendId} host=${remote.address}:${remote.port} (decrypted UDP source)`);
2180
+ }
2166
2181
  if (!state.lastUdpRecvMs) {
2167
2182
  this.#debugLog(`udp_confirmed friend=${friendId} via=${remote.address}:${remote.port} (direct UDP path live)`);
2168
2183
  }
@@ -4559,10 +4574,17 @@ export class Peer {
4559
4574
  if (payload.length >= 18) {
4560
4575
  const lanHost = `${payload[12]}.${payload[13]}.${payload[14]}.${payload[15]}`;
4561
4576
  const lanPort = ((payload[16] << 8) | payload[17]) >>> 0;
4577
+ // WSL sees only its 172.x virtual NIC, not the Windows host's physical
4578
+ // 10.x subnet. In that topology a peer on the same real LAN is still
4579
+ // safely identifiable because its server-reflexive address equals ours.
4580
+ // Permit the advertised private candidate when both peers share the same
4581
+ // public NAT; if it is a CGNAT/private collision and never validates, the
4582
+ // normal 12s stale-LAN-lock breaker restores the public/relay path.
4583
+ const samePublicNat = !!this.#srflxCache?.addr.host && this.#srflxCache.addr.host === host;
4562
4584
  const sameLan = lanPort !== 0 &&
4563
4585
  isPrivateAddress(lanHost) &&
4564
4586
  !isCgnatAddress(lanHost) &&
4565
- getPhysicalLanSubnets().some((s) => isInIpv4Subnet(lanHost, s)) &&
4587
+ (getPhysicalLanSubnets().some((s) => isInIpv4Subnet(lanHost, s)) || samePublicNat) &&
4566
4588
  !(getPhysicalLanAddresses().includes(lanHost) && this.#udp.localPort() === lanPort);
4567
4589
  if (sameLan) {
4568
4590
  this.#rememberEndpointCandidate(session, lanHost, lanPort);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/peer",
3
- "version": "0.1.104",
3
+ "version": "0.1.105",
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",