@decentnetwork/peer 0.1.74 → 0.1.75
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 +61 -13
- 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.
|
|
@@ -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).
|
|
@@ -773,7 +814,14 @@ export class FileTransferManager {
|
|
|
773
814
|
st.parityHighBlock = b;
|
|
774
815
|
continue;
|
|
775
816
|
} // clean path → skip parity
|
|
776
|
-
//
|
|
817
|
+
// Size parity to the reported loss p: to land k of (k+m) shards through a
|
|
818
|
+
// path dropping fraction p, need k+m ≥ k/(1−p) → m ≥ k·p/(1−p). Over-
|
|
819
|
+
// provision (SAFETY) and add a fixed MARGIN for burst variance; a fresh
|
|
820
|
+
// dup-ack (report not yet in) still gets FEC_M_MIN so we react immediately.
|
|
821
|
+
const p = Math.min(0.6, st.lossEwma); // cap so the divisor can't explode
|
|
822
|
+
const mNeed = Math.ceil((kActual * p / (1 - p)) * FEC_SAFETY) + FEC_MARGIN;
|
|
823
|
+
const mSend = Math.max(FEC_M_MIN, Math.min(FEC_M_MAX, mNeed));
|
|
824
|
+
// Build the k data shards (zero-padded to a full chunk) and encode mSend parity.
|
|
777
825
|
const shards = [];
|
|
778
826
|
for (let i = 0; i < kActual; i++) {
|
|
779
827
|
const off = (b * FEC_K + i) * MAX_FILE_DATA_SIZE;
|
|
@@ -786,7 +834,7 @@ export class FileTransferManager {
|
|
|
786
834
|
shards.push(padded);
|
|
787
835
|
}
|
|
788
836
|
}
|
|
789
|
-
const parity = rsEncode(shards,
|
|
837
|
+
const parity = rsEncode(shards, mSend);
|
|
790
838
|
for (let pi = 0; pi < parity.length; pi++) {
|
|
791
839
|
void this.send(friendId, PACKET_ID_FILE_FEC, encodeFec(st.fileNumber, b, pi, parity[pi])).catch(() => undefined);
|
|
792
840
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.75",
|
|
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",
|