@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.
@@ -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
- const FEC_M = 6;
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 >= FEC_M)
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(FEC_M).fill(undefined);
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 < FEC_M; pi++)
524
+ for (let pi = 0; pi < FEC_M_MAX; pi++)
513
525
  shards.push(parityArr?.[pi] ?? null);
514
- const recovered = rsDecode(shards, k, FEC_M, MAX_FILE_DATA_SIZE);
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 sender
593
- // sees the gap (maxByte contiguous) = loss/reorder, for congestion control.
594
- const body = new Uint8Array(8);
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
- // Build the k data shards (zero-padded to a full chunk) and encode M parity.
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, FEC_M);
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.74",
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",