@decentnetwork/peer 0.1.74 → 0.1.76
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 +74 -25
- package/package.json +1 -1
|
@@ -39,7 +39,16 @@ export const MAX_FILE_DATA_SIZE = 1367;
|
|
|
39
39
|
// overhead handles bursty ~15% loss with margin; the retransmit path remains a
|
|
40
40
|
// backstop for blocks that exceed FEC capacity.
|
|
41
41
|
const FEC_K = 16;
|
|
42
|
-
|
|
42
|
+
// Parity is ADAPTIVE per block: the sender sizes it to the loss rate the
|
|
43
|
+
// receiver reports (piggybacked on each ack). A fixed M=6 tolerated only
|
|
44
|
+
// ~27% loss, so a 40%-loss path (measured on lili) blew past FEC capacity and
|
|
45
|
+
// fell back to go-back-N retransmit — ~1% efficient. Now the sender sends just
|
|
46
|
+
// enough parity that blocks recover LOCALLY at the current loss, so the
|
|
47
|
+
// expensive retransmit path almost never fires.
|
|
48
|
+
const FEC_M_MIN = 4; // parity the instant loss is first detected (before the report warms up)
|
|
49
|
+
const FEC_M_MAX = 16; // ceiling on parity per K=16 block (covers ~50% loss)
|
|
50
|
+
const FEC_MARGIN = 2; // extra shards over the estimate — absorbs burst variance
|
|
51
|
+
const FEC_SAFETY = 1.15; // over-provision the loss estimate by 15%
|
|
43
52
|
// Adaptive: only spend parity bandwidth while the path is actually losing
|
|
44
53
|
// packets (a dup-ack was seen within this window). A clean path sends zero
|
|
45
54
|
// parity → no overhead; a lossy path (lili) gets full protection.
|
|
@@ -96,7 +105,7 @@ const CWND_MIN_CHUNKS = 8; // ~11 KB floor — keeps a slow/flaky path's buffer
|
|
|
96
105
|
// ~0.5s of added latency keeps ping well under a second (fine — the peer stays
|
|
97
106
|
// usable), and buys far more throughput on a jittery path than a tight 150ms
|
|
98
107
|
// bound, which collapsed the window to the floor (~17 KB/s observed on lili).
|
|
99
|
-
const MAX_QUEUE_MS = 350; // raw-queue ceiling before draining (bulk transfer tolerates ~0.35s added latency)
|
|
108
|
+
const MAX_QUEUE_MS = 350; // raw-queue ceiling before draining (bulk transfer tolerates ~0.35s added latency; a tighter bound collapses the window on a 40%-loss path where recovery makes RTT noisy)
|
|
100
109
|
const QUEUE_LOW_MS = 150; // grow while the smoothed (jitter-free) queue is under this
|
|
101
110
|
const VEGAS_GROW = 1.25; // window ×this per RTT when under-filled
|
|
102
111
|
const VEGAS_SHRINK = 0.8; // window ×this per RTT when the queue is too deep
|
|
@@ -227,7 +236,7 @@ export class FileTransferManager {
|
|
|
227
236
|
fileNumber, fileId, name: opts.name, data, size: data.length,
|
|
228
237
|
acked: 0, nextSend: 0, pumping: false, started: false, req, startMs: nowMs(),
|
|
229
238
|
lastProgressAcked: 0, lastAckAdvanceMs: nowMs(), lastResendMs: 0,
|
|
230
|
-
cwnd: CWND_INIT_CHUNKS, stalls: 0, parityHighBlock: -1, lastLossMs: 0,
|
|
239
|
+
cwnd: CWND_INIT_CHUNKS, stalls: 0, parityHighBlock: -1, lastLossMs: 0, lossEwma: 0,
|
|
231
240
|
minRttMs: Infinity, minSrttMs: Infinity, srttMs: 0, probeOffset: -1, probeSentMs: 0,
|
|
232
241
|
};
|
|
233
242
|
this.#map(this.#sending, friendId).set(fileNumber, st);
|
|
@@ -317,7 +326,7 @@ export class FileTransferManager {
|
|
|
317
326
|
} // re-offer (our accept/ack lost) → re-ack
|
|
318
327
|
const st = {
|
|
319
328
|
fileNumber: r.fileNumber, fileId: r.fileId, name: r.filename, size,
|
|
320
|
-
buf: new Uint8Array(size), got: new Uint8Array(chunkCount(size)),
|
|
329
|
+
buf: new Uint8Array(size), got: new Uint8Array(chunkCount(size)), netGot: new Uint8Array(chunkCount(size)), lossPctEwma: 0,
|
|
321
330
|
contiguous: 0, maxByte: 0, persisted: 0, partPath: this.#partPath(r.fileId), fecParity: new Map(), done: false, ackDirty: false,
|
|
322
331
|
};
|
|
323
332
|
// 断点续传: if we persisted a contiguous prefix of this exact file (matched
|
|
@@ -334,8 +343,10 @@ export class FileTransferManager {
|
|
|
334
343
|
if (usable > 0) {
|
|
335
344
|
const partial = readFileSync(st.partPath);
|
|
336
345
|
st.buf.set(partial.subarray(0, usable), 0);
|
|
337
|
-
for (let i = 0; i < chunks; i++)
|
|
346
|
+
for (let i = 0; i < chunks; i++) {
|
|
338
347
|
st.got[i] = 1;
|
|
348
|
+
st.netGot[i] = 1;
|
|
349
|
+
}
|
|
339
350
|
st.contiguous = usable;
|
|
340
351
|
st.maxByte = usable;
|
|
341
352
|
st.persisted = usable;
|
|
@@ -416,6 +427,7 @@ export class FileTransferManager {
|
|
|
416
427
|
return; // not a chunk boundary
|
|
417
428
|
if (offset + data.length > st.maxByte)
|
|
418
429
|
st.maxByte = offset + data.length;
|
|
430
|
+
st.netGot[idx] = 1; // genuine network arrival (even if a dup) → raw-loss denominator
|
|
419
431
|
if (!st.got[idx]) {
|
|
420
432
|
st.buf.set(data, offset);
|
|
421
433
|
st.got[idx] = 1;
|
|
@@ -455,11 +467,11 @@ export class FileTransferManager {
|
|
|
455
467
|
const st = this.#map(this.#receiving, friendId).get(f.fileNumber);
|
|
456
468
|
if (!st || st.done)
|
|
457
469
|
return;
|
|
458
|
-
if (f.parity.length !== MAX_FILE_DATA_SIZE || f.parityIndex >=
|
|
470
|
+
if (f.parity.length !== MAX_FILE_DATA_SIZE || f.parityIndex >= FEC_M_MAX)
|
|
459
471
|
return;
|
|
460
472
|
let arr = st.fecParity.get(f.blockIndex);
|
|
461
473
|
if (!arr) {
|
|
462
|
-
arr = new Array(
|
|
474
|
+
arr = new Array(FEC_M_MAX).fill(undefined);
|
|
463
475
|
st.fecParity.set(f.blockIndex, arr);
|
|
464
476
|
}
|
|
465
477
|
if (!arr[f.parityIndex])
|
|
@@ -509,9 +521,9 @@ export class FileTransferManager {
|
|
|
509
521
|
else
|
|
510
522
|
shards.push(null);
|
|
511
523
|
}
|
|
512
|
-
for (let pi = 0; pi <
|
|
524
|
+
for (let pi = 0; pi < FEC_M_MAX; pi++)
|
|
513
525
|
shards.push(parityArr?.[pi] ?? null);
|
|
514
|
-
const recovered = rsDecode(shards, k,
|
|
526
|
+
const recovered = rsDecode(shards, k, FEC_M_MAX, MAX_FILE_DATA_SIZE);
|
|
515
527
|
if (!recovered)
|
|
516
528
|
return;
|
|
517
529
|
for (const i of missing) {
|
|
@@ -586,14 +598,37 @@ export class FileTransferManager {
|
|
|
586
598
|
this.#sendAck(friendId, st);
|
|
587
599
|
}, ACK_THROTTLE_MS);
|
|
588
600
|
}
|
|
601
|
+
// Raw network loss (0..100) behind the frontier: the fraction of chunk slots
|
|
602
|
+
// between the contiguous prefix and the highest byte seen that have NOT arrived
|
|
603
|
+
// over the network (netGot=0). FEC-recovered holes still count as losses here
|
|
604
|
+
// (netGot stays 0), so the estimate doesn't collapse to zero the moment FEC
|
|
605
|
+
// patches a block — which would starve parity and reopen the hole. Self-
|
|
606
|
+
// correcting: under-provisioned parity leaves holes → the window fills with
|
|
607
|
+
// gaps → the reported loss rises → the sender adds parity.
|
|
608
|
+
#measureLoss(st) {
|
|
609
|
+
const lo = Math.floor(st.contiguous / MAX_FILE_DATA_SIZE);
|
|
610
|
+
const hi = Math.min(st.got.length, Math.ceil(st.maxByte / MAX_FILE_DATA_SIZE));
|
|
611
|
+
let span = 0, holes = 0;
|
|
612
|
+
for (let i = lo; i < hi && span < 4096; i++) {
|
|
613
|
+
span++;
|
|
614
|
+
if (!st.netGot[i])
|
|
615
|
+
holes++;
|
|
616
|
+
}
|
|
617
|
+
const inst = span > 0 ? holes / span : 0;
|
|
618
|
+
st.lossPctEwma = st.lossPctEwma === 0 ? inst : 0.7 * st.lossPctEwma + 0.3 * inst;
|
|
619
|
+
return Math.max(0, Math.min(100, Math.round(st.lossPctEwma * 100)));
|
|
620
|
+
}
|
|
589
621
|
#sendAck(friendId, st) {
|
|
590
622
|
st.ackDirty = false;
|
|
591
623
|
// send_receive=1: the ACK refers to a file the PEER is SENDING (it lives in
|
|
592
|
-
// the peer's #sending table). Carries [contiguous][maxByte] so the
|
|
593
|
-
// sees the gap (
|
|
594
|
-
|
|
624
|
+
// the peer's #sending table). Carries [contiguous][maxByte][lossPct] so the
|
|
625
|
+
// sender sees the gap (for congestion control) and the raw loss rate (to size
|
|
626
|
+
// adaptive FEC parity). The 9th byte is additive — a pre-0.1.75 sender that
|
|
627
|
+
// reads only 8 bytes just ignores it.
|
|
628
|
+
const body = new Uint8Array(9);
|
|
595
629
|
body.set(u32be(st.contiguous), 0);
|
|
596
630
|
body.set(u32be(st.maxByte), 4);
|
|
631
|
+
body[8] = this.#measureLoss(st);
|
|
597
632
|
void this.send(friendId, PACKET_ID_FILE_CONTROL, encodeFileControl(1, st.fileNumber, FILECONTROL.ACK, body)).catch(() => undefined);
|
|
598
633
|
}
|
|
599
634
|
// ── sender ────────────────────────────────────────────────────────
|
|
@@ -602,6 +637,12 @@ export class FileTransferManager {
|
|
|
602
637
|
return;
|
|
603
638
|
const ackedOffset = readU32be(data, 0);
|
|
604
639
|
const maxRecv = data.length >= 8 ? readU32be(data, 4) : ackedOffset;
|
|
640
|
+
// Receiver-reported raw loss (0..100) → drives adaptive FEC parity. Lightly
|
|
641
|
+
// re-smoothed here so a single ack can't swing the parity budget.
|
|
642
|
+
if (data.length >= 9) {
|
|
643
|
+
const reported = data[8] / 100;
|
|
644
|
+
st.lossEwma = 0.6 * st.lossEwma + 0.4 * reported;
|
|
645
|
+
}
|
|
605
646
|
if (ackedOffset > st.acked) {
|
|
606
647
|
// Progress. The path is delivering → additively re-open the window (up to
|
|
607
648
|
// the cap) and clear the stall streak (patience shrinks back to the base).
|
|
@@ -633,12 +674,8 @@ export class FileTransferManager {
|
|
|
633
674
|
if (st.srttMs < st.minSrttMs)
|
|
634
675
|
st.minSrttMs = st.srttMs;
|
|
635
676
|
// Hybrid signal, so per-packet JITTER doesn't masquerade as a queue:
|
|
636
|
-
// • GROW on the SMOOTHED queue (srtt − minSrtt): jitter averages out
|
|
637
|
-
//
|
|
638
|
-
// of collapsing to the floor (~17 KB/s observed on lili).
|
|
639
|
-
// • SHRINK on the RAW queue (rtt − minRtt) against a higher ceiling:
|
|
640
|
-
// fast reaction to genuine bufferbloat, but the ceiling sits above
|
|
641
|
-
// the jitter band so a lone jittery sample doesn't trigger it.
|
|
677
|
+
// • GROW on the SMOOTHED queue (srtt − minSrtt): jitter averages out.
|
|
678
|
+
// • SHRINK on the RAW queue (rtt − minRtt) against a higher ceiling.
|
|
642
679
|
const smoothQueue = st.minSrttMs !== Infinity ? st.srttMs - st.minSrttMs : 0;
|
|
643
680
|
const rawQueue = st.minRttMs !== Infinity ? rtt - st.minRttMs : 0;
|
|
644
681
|
if (rawQueue > MAX_QUEUE_MS) {
|
|
@@ -654,11 +691,16 @@ export class FileTransferManager {
|
|
|
654
691
|
this.emit("file-progress", { friendId, fileId: hex(st.fileId), received: st.acked, total: st.size, sending: true });
|
|
655
692
|
}
|
|
656
693
|
}
|
|
657
|
-
else if (maxRecv > st.acked + MAX_FILE_DATA_SIZE && nowMs() - st.lastResendMs >= FAST_RT_MIN_MS) {
|
|
658
|
-
// Duplicate ack WITH a gap
|
|
659
|
-
//
|
|
660
|
-
//
|
|
661
|
-
//
|
|
694
|
+
else if (maxRecv > st.acked + Math.max(4, st.cwnd >> 2) * MAX_FILE_DATA_SIZE && nowMs() - st.lastResendMs >= FAST_RT_MIN_MS) {
|
|
695
|
+
// Duplicate ack WITH a gap bigger than the REORDER window (≈¼ of the flight)
|
|
696
|
+
// → the chunk at `acked` is genuinely lost, not just reordered. Requiring a
|
|
697
|
+
// real gap is what unlocked throughput on jittery GFW paths: a 1-chunk
|
|
698
|
+
// trigger fired constantly on per-packet jitter (packets arrive out of
|
|
699
|
+
// order), each firing a go-back-N rewind that re-sent bytes the receiver
|
|
700
|
+
// already had and pinned the window near the floor. Now FEC repairs the
|
|
701
|
+
// occasional true hole locally, so waiting a few chunks before retransmit
|
|
702
|
+
// costs nothing. Rewind the send cursor to the hole (rate-limited to ~1 RTT
|
|
703
|
+
// so a dup-ack burst can't storm). No window backoff: loss ≠ congestion.
|
|
662
704
|
st.lastResendMs = nowMs();
|
|
663
705
|
st.lastLossMs = nowMs(); // the path is lossy → turn adaptive FEC on
|
|
664
706
|
if (st.nextSend > st.acked)
|
|
@@ -773,7 +815,14 @@ export class FileTransferManager {
|
|
|
773
815
|
st.parityHighBlock = b;
|
|
774
816
|
continue;
|
|
775
817
|
} // clean path → skip parity
|
|
776
|
-
//
|
|
818
|
+
// Size parity to the reported loss p: to land k of (k+m) shards through a
|
|
819
|
+
// path dropping fraction p, need k+m ≥ k/(1−p) → m ≥ k·p/(1−p). Over-
|
|
820
|
+
// provision (SAFETY) and add a fixed MARGIN for burst variance; a fresh
|
|
821
|
+
// dup-ack (report not yet in) still gets FEC_M_MIN so we react immediately.
|
|
822
|
+
const p = Math.min(0.6, st.lossEwma); // cap so the divisor can't explode
|
|
823
|
+
const mNeed = Math.ceil((kActual * p / (1 - p)) * FEC_SAFETY) + FEC_MARGIN;
|
|
824
|
+
const mSend = Math.max(FEC_M_MIN, Math.min(FEC_M_MAX, mNeed));
|
|
825
|
+
// Build the k data shards (zero-padded to a full chunk) and encode mSend parity.
|
|
777
826
|
const shards = [];
|
|
778
827
|
for (let i = 0; i < kActual; i++) {
|
|
779
828
|
const off = (b * FEC_K + i) * MAX_FILE_DATA_SIZE;
|
|
@@ -786,7 +835,7 @@ export class FileTransferManager {
|
|
|
786
835
|
shards.push(padded);
|
|
787
836
|
}
|
|
788
837
|
}
|
|
789
|
-
const parity = rsEncode(shards,
|
|
838
|
+
const parity = rsEncode(shards, mSend);
|
|
790
839
|
for (let pi = 0; pi < parity.length; pi++) {
|
|
791
840
|
void this.send(friendId, PACKET_ID_FILE_FEC, encodeFec(st.fileNumber, b, pi, parity[pi])).catch(() => undefined);
|
|
792
841
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.76",
|
|
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",
|