@decentnetwork/peer 0.1.54 → 0.1.56
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.
- package/dist/compat/filetransfer.js +39 -43
- package/dist/peer.js +34 -7
- package/package.json +1 -1
|
@@ -138,7 +138,7 @@ export class FileTransferManager {
|
|
|
138
138
|
const req = encodeFileSendRequest(fileNumber, opts.kind ?? FILEKIND.DATA, BigInt(data.length), fileId, opts.name);
|
|
139
139
|
const st = {
|
|
140
140
|
fileNumber, fileId, name: opts.name, data, size: data.length,
|
|
141
|
-
acked: 0,
|
|
141
|
+
acked: 0, nextSend: 0, pumping: false, started: false, req, startMs: nowMs(),
|
|
142
142
|
lastProgressAcked: 0, lastAckAdvanceMs: nowMs(), lastResendMs: 0,
|
|
143
143
|
};
|
|
144
144
|
this.#map(this.#sending, friendId).set(fileNumber, st);
|
|
@@ -369,23 +369,27 @@ export class FileTransferManager {
|
|
|
369
369
|
}
|
|
370
370
|
}
|
|
371
371
|
else if (maxRecv > st.acked + MAX_FILE_DATA_SIZE && nowMs() - st.lastResendMs >= FAST_RT_MIN_MS) {
|
|
372
|
-
// Duplicate ack WITH a gap → the chunk at `acked` was lost.
|
|
373
|
-
// the
|
|
374
|
-
// dup-
|
|
375
|
-
// is treated as random, not congestion.
|
|
372
|
+
// Duplicate ack WITH a gap → the chunk at `acked` was lost. Rewind the send
|
|
373
|
+
// cursor to the ack point so the loop re-sends the hole (rate-limited to
|
|
374
|
+
// ~1 RTT so dup-ack bursts for one loss don't storm). No window backoff:
|
|
375
|
+
// file-path loss is treated as random, not congestion.
|
|
376
376
|
st.lastResendMs = nowMs();
|
|
377
|
-
|
|
377
|
+
if (st.nextSend > st.acked)
|
|
378
|
+
st.nextSend = st.acked;
|
|
378
379
|
}
|
|
379
380
|
if (st.acked >= st.size) {
|
|
380
381
|
this.#completeSend(friendId, st);
|
|
381
382
|
return;
|
|
382
383
|
}
|
|
383
|
-
this.#pump(friendId, st);
|
|
384
|
+
void this.#pump(friendId, st);
|
|
384
385
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
386
|
+
// Single send loop. Awaits each send so net_crypto / the UDP socket applies
|
|
387
|
+
// backpressure (fire-and-forget blasted the whole window into the macOS UDP
|
|
388
|
+
// buffer at once → systematic overflow/drops the retransmit couldn't dig out
|
|
389
|
+
// of → a big file "failed"). Sends from `nextSend`, which the ack handler and
|
|
390
|
+
// watchdog rewind to `acked` to retransmit a hole (go-back-N). The fixed
|
|
391
|
+
// window bounds in-flight bytes; throughput self-clocks to the path.
|
|
392
|
+
async #pump(friendId, st) {
|
|
389
393
|
if (!st.timer) {
|
|
390
394
|
let stalledSinceMs = nowMs();
|
|
391
395
|
st.timer = setInterval(() => {
|
|
@@ -408,44 +412,36 @@ export class FileTransferManager {
|
|
|
408
412
|
return;
|
|
409
413
|
}
|
|
410
414
|
st.lastAckAdvanceMs = nowMs();
|
|
411
|
-
st.highestSent = st.acked; // go-back-N: re-send from the ack point
|
|
412
415
|
st.lastResendMs = nowMs();
|
|
413
|
-
|
|
416
|
+
if (st.nextSend > st.acked)
|
|
417
|
+
st.nextSend = st.acked; // go-back-N
|
|
418
|
+
void this.#pump(friendId, st);
|
|
414
419
|
}, WATCHDOG_MS);
|
|
415
420
|
if (typeof st.timer.unref === "function")
|
|
416
421
|
st.timer.unref();
|
|
417
422
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
423
|
+
if (st.pumping)
|
|
424
|
+
return;
|
|
425
|
+
st.pumping = true;
|
|
426
|
+
try {
|
|
427
|
+
while (this.#map(this.#sending, friendId).has(st.fileNumber) && st.acked < st.size) {
|
|
428
|
+
const windowEnd = Math.min(st.acked + WINDOW_CHUNKS * MAX_FILE_DATA_SIZE, st.size);
|
|
429
|
+
if (st.nextSend >= windowEnd)
|
|
430
|
+
break; // window full / all sent — wait for acks
|
|
431
|
+
const off = st.nextSend;
|
|
432
|
+
const end = Math.min(off + MAX_FILE_DATA_SIZE, st.size);
|
|
433
|
+
try {
|
|
434
|
+
await this.send(friendId, PACKET_ID_FILE_DATA, encodeFileData(st.fileNumber, off, st.data.subarray(off, end)));
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
break; // transport down; the watchdog re-kicks
|
|
438
|
+
}
|
|
439
|
+
if (st.nextSend === off)
|
|
440
|
+
st.nextSend = end; // unless a rewind moved it
|
|
441
|
+
}
|
|
434
442
|
}
|
|
435
|
-
|
|
436
|
-
st.
|
|
437
|
-
}
|
|
438
|
-
// Re-send the chunks in [from, to) — the gap region the receiver is missing.
|
|
439
|
-
// Already-received chunks are idempotent (placed by offset on the receiver).
|
|
440
|
-
#resendRange(friendId, st, from, to) {
|
|
441
|
-
let off = from;
|
|
442
|
-
const end0 = Math.min(to, st.size);
|
|
443
|
-
while (off < end0) {
|
|
444
|
-
if (!this.#map(this.#sending, friendId).has(st.fileNumber))
|
|
445
|
-
return;
|
|
446
|
-
const end = Math.min(off + MAX_FILE_DATA_SIZE, st.size);
|
|
447
|
-
void this.send(friendId, PACKET_ID_FILE_DATA, encodeFileData(st.fileNumber, off, st.data.subarray(off, end))).catch(() => undefined);
|
|
448
|
-
off = end;
|
|
443
|
+
finally {
|
|
444
|
+
st.pumping = false;
|
|
449
445
|
}
|
|
450
446
|
}
|
|
451
447
|
#completeSend(friendId, st) {
|
package/dist/peer.js
CHANGED
|
@@ -1470,13 +1470,21 @@ export class Peer {
|
|
|
1470
1470
|
this.#debugLog(`hs_recv friend=${friendId} initiated=${weInitiated ? 1 : 0} remote=${remote.address}:${remote.port}`);
|
|
1471
1471
|
if (!wasEstablished) {
|
|
1472
1472
|
this.#debugLog(`friend_connected friend=${friendId} remote=${remote.address}:${remote.port}`);
|
|
1473
|
-
//
|
|
1474
|
-
//
|
|
1475
|
-
//
|
|
1476
|
-
//
|
|
1477
|
-
//
|
|
1478
|
-
//
|
|
1479
|
-
|
|
1473
|
+
// Send our UDP endpoint offer (public srflx + LAN host candidate)
|
|
1474
|
+
// whenever the current path isn't already a same-LAN direct one:
|
|
1475
|
+
// - relay-only sessions need it to bootstrap a direct UDP path; and
|
|
1476
|
+
// - sessions that came up over a PUBLIC UDP endpoint (internet
|
|
1477
|
+
// hairpin) when both peers are actually on the SAME LAN — without
|
|
1478
|
+
// the offer they never learn each other's 10.x/192.168.x address and
|
|
1479
|
+
// stay stuck on the public path, which a symmetric / hairpin NAT
|
|
1480
|
+
// breaks one-way (observed: two boxes on one LAN where each only saw
|
|
1481
|
+
// the other one-directionally). The peer's same-subnet filter drops
|
|
1482
|
+
// the LAN candidate when it doesn't apply, so offering is always safe.
|
|
1483
|
+
const r = state.remote;
|
|
1484
|
+
const remoteIsSameLan = !!r && isPrivateAddress(r.host) && !isCgnatAddress(r.host) &&
|
|
1485
|
+
getPhysicalLanSubnets().some((s) => isInIpv4Subnet(r.host, s));
|
|
1486
|
+
const haveLanToOffer = getPhysicalLanAddresses().some((ip) => isPrivateAddress(ip) && !isCgnatAddress(ip));
|
|
1487
|
+
if ((state.hasTcpRoute && !r) || (!remoteIsSameLan && haveLanToOffer)) {
|
|
1480
1488
|
void this.#sendUdpEndpointOffer(friendId).catch(() => undefined);
|
|
1481
1489
|
}
|
|
1482
1490
|
}
|
|
@@ -3961,6 +3969,25 @@ export class Peer {
|
|
|
3961
3969
|
if (!node.isTcp)
|
|
3962
3970
|
void this.#sendDhtPing(node);
|
|
3963
3971
|
}
|
|
3972
|
+
// 4. LAN-direct upgrade. Re-offer our endpoint (public srflx + LAN host
|
|
3973
|
+
// candidate) to any established friend whose current path is NOT already
|
|
3974
|
+
// a same-LAN address, as long as we have a LAN address to advertise. This
|
|
3975
|
+
// upgrades sessions that came up over a public UDP endpoint (internet
|
|
3976
|
+
// hairpin) to the direct LAN path even without a re-establish, and self-
|
|
3977
|
+
// stops once the path becomes same-LAN. The peer's same-subnet filter
|
|
3978
|
+
// drops the candidate when it doesn't apply, so this is always safe.
|
|
3979
|
+
const haveLan = getPhysicalLanAddresses().some((ip) => isPrivateAddress(ip) && !isCgnatAddress(ip));
|
|
3980
|
+
if (haveLan) {
|
|
3981
|
+
for (const [friendId, session] of this.#friendSessions.entries()) {
|
|
3982
|
+
if (!session.established)
|
|
3983
|
+
continue;
|
|
3984
|
+
const r = session.remote;
|
|
3985
|
+
const remoteIsSameLan = !!r && isPrivateAddress(r.host) && !isCgnatAddress(r.host) &&
|
|
3986
|
+
getPhysicalLanSubnets().some((s) => isInIpv4Subnet(r.host, s));
|
|
3987
|
+
if (!remoteIsSameLan)
|
|
3988
|
+
void this.#sendUdpEndpointOffer(friendId).catch(() => undefined);
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3964
3991
|
}
|
|
3965
3992
|
/**
|
|
3966
3993
|
* Send an onion DHT-PK announcement to a friend so they learn our DHT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.56",
|
|
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",
|