@decentnetwork/peer 0.1.97 → 0.1.98
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 +96 -49
- package/package.json +1 -1
|
@@ -86,48 +86,43 @@ export const FILEKIND = { DATA: 0, AVATAR: 1 };
|
|
|
86
86
|
const WINDOW_CHUNKS = 768; // ~1 MB — the window CAP (fills a 200ms-RTT path at ~5 MB/s)
|
|
87
87
|
const CWND_INIT_CHUNKS = 16; // ~22 KB start — small so the opening burst can't bloat a thin path
|
|
88
88
|
const CWND_MIN_CHUNKS = 8; // ~11 KB floor — keeps a slow/flaky path's buffer shallow
|
|
89
|
-
// Congestion control (TCP Vegas — DELAY-based). Loss/AIMD control fails on a
|
|
90
|
-
// slow peer with a deep buffer (a user's laptop): ~1 MB in flight piles up as
|
|
91
|
-
// 12-23 s of queue latency with NO loss, so it never backs off, and the stalls
|
|
92
|
-
// trigger go-back-N resends that balloon the queue further. Vegas instead reads
|
|
93
|
-
// how many chunks are QUEUED from the RTT inflation — cwnd × (1 − minRtt/RTT) —
|
|
94
|
-
// and steers the window to keep that between ALPHA and BETA. When the queue is
|
|
95
|
-
// below ALPHA there's spare capacity → grow (no self-limiting, unlike a
|
|
96
|
-
// rate-based cap); above BETA the queue is too deep → shrink. It converges to
|
|
97
|
-
// "pipe full + a couple chunks queued" on ANY path: a fast link gets a big
|
|
98
|
-
// window, a thin flaky one a small window and a shallow queue, automatically.
|
|
99
|
-
// Target the ABSOLUTE queue delay (rtt − minRtt), not a packet count: bound the
|
|
100
|
-
// latency the transfer ADDS to the path to ≈ MAX_QUEUE_MS on ANY path. A fast
|
|
101
|
-
// link keeps a big window (it drains quickly, so a big window adds little
|
|
102
|
-
// delay); a thin flaky one keeps a small window (a big window there would add
|
|
103
|
-
// seconds). Growing whenever the added delay is low means no self-limiting.
|
|
104
|
-
// The budget is generous on purpose: for a bulk transfer to a flaky peer,
|
|
105
|
-
// ~0.5s of added latency keeps ping well under a second (fine — the peer stays
|
|
106
|
-
// usable), and buys far more throughput on a jittery path than a tight 150ms
|
|
107
|
-
// bound, which collapsed the window to the floor (~17 KB/s observed on lili).
|
|
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)
|
|
109
|
-
const QUEUE_LOW_MS = 150; // grow while the smoothed (jitter-free) queue is under this
|
|
110
|
-
const VEGAS_GROW = 1.25; // window ×this per RTT when under-filled
|
|
111
|
-
const VEGAS_SHRINK = 0.8; // window ×this per RTT when the queue is too deep
|
|
112
89
|
// PACED control (BBR-style). The window-based Vegas controller blasted the whole
|
|
113
90
|
// cwnd into the buffer each round, which on a DEEP buffer (the TCP relay's
|
|
114
91
|
// China↔US queue) piled up to 7-15 s of latency — the feedback loop is longer
|
|
115
92
|
// than a human's patience, so it can't react. The fix is to PACE: send at the
|
|
116
93
|
// estimated bottleneck bandwidth so the buffer stays near-empty regardless of
|
|
117
94
|
// its depth, and RAMP the rate in a startup phase to fill the pipe for speed.
|
|
118
|
-
const PACE_STARTUP_GAIN = 2.0; // startup: pace at 2× measured rate to probe up fast (exits when bandwidth plateaus)
|
|
119
95
|
const PACE_CYCLE_GAINS = [1.25, 0.8, 1, 1, 1, 1, 1, 1]; // steady PROBE_BW cycle: one round up, one round DRAINS the queue, six cruise → avg≈bandwidth, shallow queue
|
|
120
|
-
const PACE_FULLBW_GROWTH = 1.20; // bandwidth still "rising" if ≥20% over the plateau mark
|
|
121
|
-
const PACE_FULLBW_ROUNDS = 3; // leave startup after this many non-rising rounds
|
|
122
96
|
const PACE_BW_WINDOW = 10; // rounds of delivered-rate history for the max-filter
|
|
123
97
|
const PACE_SAMPLE_MS = 200; // aggregate ACKs so one scheduler-jittered packet is not a "round"
|
|
124
98
|
const PACE_INIT_BPS = 120_000; // initial pace rate (~120 KB/s) before any bandwidth estimate
|
|
125
99
|
const PACE_BURST_MS = 20; // token-bucket burst allowance (keeps sends smooth, not bunched)
|
|
100
|
+
// A LAN receiver can coalesce a backlog of ACKs after its event loop wakes. The
|
|
101
|
+
// resulting delivery sample is ACK rate, not link capacity: in practice a
|
|
102
|
+
// 4 MB/s Windows path briefly reported 23 MB/s, startup chased that sample,
|
|
103
|
+
// overflowed the receiver, and then collapsed to the minimum window. Bound how
|
|
104
|
+
// quickly a validated-LAN estimate and startup pace may grow. Real capacity is
|
|
105
|
+
// still discovered quickly (25% every 200 ms), without one ACK burst taking
|
|
106
|
+
// over the controller.
|
|
107
|
+
const LAN_SAMPLE_MAX_GAIN = 1.25;
|
|
108
|
+
const LAN_STARTUP_MAX_GAIN = 1.15;
|
|
109
|
+
const LAN_STARTUP_DELIVERY_GAIN = 1.5; // never probe above 1.5x recently delivered goodput
|
|
110
|
+
const LAN_STARTUP_QUEUE_MS = 500; // unmistakable raw queue spike; smaller app-level RTT outliers are normal on Windows
|
|
111
|
+
const LAN_STARTUP_KEEPUP = 0.70; // delivered goodput below 70% of pace means the receiver is no longer keeping up
|
|
112
|
+
const LAN_STARTUP_SLOW_ROUNDS = 5; // require sustained saturation across Windows scheduler/ACK jitter
|
|
113
|
+
// FILE ACKs are coalesced and handled by the remote JS event loop. On Windows
|
|
114
|
+
// the effective ACK clock is commonly 20–120 ms even when an occasional probe
|
|
115
|
+
// reports a 7 ms network RTT. Sizing cwnd from that lucky minimum made a
|
|
116
|
+
// 1.7 MB/s pacer use an 18-chunk (~24 KB) window, which itself capped goodput
|
|
117
|
+
// and falsely ended startup. Pacing remains the primary throttle, so this floor
|
|
118
|
+
// only prevents the safety window from starving a validated LAN flow.
|
|
119
|
+
const LAN_BDP_RTT_FLOOR_MS = 50;
|
|
126
120
|
const WATCHDOG_MS = 150; // stall poll cadence / base no-progress patience
|
|
127
121
|
const WATCHDOG_MAX_MS = 8000; // cap the RTT-adaptive stall patience (must exceed a slow path's RTT so acks aren't mistaken for a stall)
|
|
128
122
|
const FAST_RT_MIN_MS = 40; // min gap between fast-retransmits of the same hole (≈1 RTT)
|
|
129
123
|
const ACK_THROTTLE_MS = 15; // coalesce receiver acks (fast enough to drive fast-retransmit)
|
|
130
124
|
const REACK_MS = 200; // receiver keep-alive re-ack cadence
|
|
125
|
+
const FINAL_ACK_GRACE_MS = 10000; // retry final cumulative ACK across loss/reorder/short session handover
|
|
131
126
|
const SEND_GIVEUP_MS = 60000; // abandon a send after this long with no ack progress (peer gone)
|
|
132
127
|
const PERSIST_INTERVAL_BYTES = 1024 * 1024; // flush the resume file at most once per this much progress
|
|
133
128
|
function u32be(n) {
|
|
@@ -253,9 +248,9 @@ export class FileTransferManager {
|
|
|
253
248
|
acked: 0, nextSend: 0, pumping: false, started: false, req, startMs: nowMs(),
|
|
254
249
|
lastProgressAcked: 0, lastAckAdvanceMs: nowMs(), lastResendMs: 0,
|
|
255
250
|
cwnd: CWND_INIT_CHUNKS, stalls: 0, parityHighBlock: -1, lastLossMs: 0, lossEwma: 0,
|
|
256
|
-
minRttMs: Infinity,
|
|
251
|
+
minRttMs: Infinity, srttMs: 0, probeOffset: -1, probeSentMs: 0,
|
|
257
252
|
paceRateBps: PACE_INIT_BPS, paceTokens: 0, lastPaceMs: nowMs(),
|
|
258
|
-
btlBwBps: 0, bwSamples: [],
|
|
253
|
+
btlBwBps: 0, bwSamples: [], fullBwRounds: 0, bbrPhase: 0, roundCount: 0,
|
|
259
254
|
rateMarkMs: nowMs(), rateMarkAcked: 0, lowRttCount: 0, lowRttMinMs: Infinity, lastCcLogMs: 0,
|
|
260
255
|
lanPath: this.isLanPath(friendId),
|
|
261
256
|
};
|
|
@@ -599,7 +594,25 @@ export class FileTransferManager {
|
|
|
599
594
|
clearInterval(st.reackTimer);
|
|
600
595
|
st.reackTimer = undefined;
|
|
601
596
|
}
|
|
602
|
-
this.#sendAck(friendId, st); // final ack so the sender completes
|
|
597
|
+
this.#sendAck(friendId, st); // first final ack so the sender completes
|
|
598
|
+
// A final ACK used to be the only ACK we sent exactly once. If it landed
|
|
599
|
+
// during UDP loss, packet reordering, or a short crypto-session handover,
|
|
600
|
+
// the receiver had the complete byte-exact file but the sender/UI remained
|
|
601
|
+
// stuck at its previous cumulative offset until timeout. Keep retrying the
|
|
602
|
+
// tiny cumulative ACK while the lightweight done-marker exists. This is
|
|
603
|
+
// idempotent, and the sender drops its state after the first one it receives.
|
|
604
|
+
st.reackTimer = setInterval(() => {
|
|
605
|
+
const cur = this.#map(this.#receiving, friendId).get(st.fileNumber);
|
|
606
|
+
if (cur !== st) {
|
|
607
|
+
if (st.reackTimer)
|
|
608
|
+
clearInterval(st.reackTimer);
|
|
609
|
+
st.reackTimer = undefined;
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
this.#sendAck(friendId, st);
|
|
613
|
+
}, REACK_MS);
|
|
614
|
+
if (typeof st.reackTimer.unref === "function")
|
|
615
|
+
st.reackTimer.unref();
|
|
603
616
|
const data = st.buf;
|
|
604
617
|
// Transfer complete → the resume file is no longer needed.
|
|
605
618
|
if (st.partPath) {
|
|
@@ -616,9 +629,14 @@ export class FileTransferManager {
|
|
|
616
629
|
st.got = new Uint8Array(0);
|
|
617
630
|
const t = setTimeout(() => {
|
|
618
631
|
const cur = this.#map(this.#receiving, friendId).get(st.fileNumber);
|
|
619
|
-
if (cur === st)
|
|
632
|
+
if (cur === st) {
|
|
633
|
+
if (st.reackTimer) {
|
|
634
|
+
clearInterval(st.reackTimer);
|
|
635
|
+
st.reackTimer = undefined;
|
|
636
|
+
}
|
|
620
637
|
this.#map(this.#receiving, friendId).delete(st.fileNumber);
|
|
621
|
-
|
|
638
|
+
}
|
|
639
|
+
}, FINAL_ACK_GRACE_MS);
|
|
622
640
|
if (typeof t.unref === "function")
|
|
623
641
|
t.unref();
|
|
624
642
|
}
|
|
@@ -745,21 +763,29 @@ export class FileTransferManager {
|
|
|
745
763
|
const roundRate = ((st.acked - st.rateMarkAcked) * 1000) / sampleMs;
|
|
746
764
|
st.rateMarkMs = nowT;
|
|
747
765
|
st.rateMarkAcked = st.acked;
|
|
748
|
-
|
|
749
|
-
|
|
766
|
+
const lanNow = this.isLanPath(friendId);
|
|
767
|
+
// On a paced LAN flow, sustainable delivery cannot suddenly be
|
|
768
|
+
// many times faster than the bytes we are putting on the wire.
|
|
769
|
+
// Such a sample is delayed/coalesced ACKs draining, not new
|
|
770
|
+
// capacity. Relay samples keep their existing conservative cap.
|
|
771
|
+
const measuredRate = lanNow
|
|
772
|
+
? Math.min(roundRate, Math.max(PACE_INIT_BPS, st.paceRateBps * LAN_SAMPLE_MAX_GAIN))
|
|
773
|
+
: roundRate;
|
|
774
|
+
if (measuredRate > 0) {
|
|
775
|
+
st.bwSamples.push(measuredRate);
|
|
750
776
|
if (st.bwSamples.length > PACE_BW_WINDOW)
|
|
751
777
|
st.bwSamples.shift();
|
|
752
778
|
st.btlBwBps = Math.max(...st.bwSamples);
|
|
753
779
|
}
|
|
754
|
-
//
|
|
755
|
-
//
|
|
756
|
-
|
|
757
|
-
|
|
780
|
+
// On a lossy public/relay path, wire carries goodput + FEC parity +
|
|
781
|
+
// retransmits, so compensate by 1/(1-loss). On a physical LAN,
|
|
782
|
+
// loss is normally self-induced receiver/socket overflow; raising
|
|
783
|
+
// pace in response creates a positive feedback loop (up to 2x).
|
|
784
|
+
const lossMult = lanNow ? 1 : 1 / (1 - Math.min(0.5, st.lossEwma));
|
|
758
785
|
if (lanNow && !st.lanPath) {
|
|
759
786
|
// A transfer can begin on relay and then promote to a validated
|
|
760
787
|
// physical-LAN endpoint. Restart STARTUP from the measured rate.
|
|
761
788
|
st.bbrPhase = 0;
|
|
762
|
-
st.fullBwBps = 0;
|
|
763
789
|
st.fullBwRounds = 0;
|
|
764
790
|
}
|
|
765
791
|
st.lanPath = lanNow;
|
|
@@ -778,22 +804,37 @@ export class FileTransferManager {
|
|
|
778
804
|
// 1 ms → 2 ms scheduler jitter looked like a 100% queue and pinned
|
|
779
805
|
// every transfer near PACE_INIT_BPS. The absolute, smoothed queue
|
|
780
806
|
// ceiling still protects a deep relay from runaway buffering.
|
|
781
|
-
|
|
782
|
-
|
|
807
|
+
// A paced flow initially delivers slightly less than its wire
|
|
808
|
+
// pace, so max-filter plateau detection can exit at 120 KB/s
|
|
809
|
+
// before it has actually probed the LAN. Instead, keep ramping
|
|
810
|
+
// while the receiver sustains most of the offered rate. Stop
|
|
811
|
+
// only after several under-delivery samples or a clear queue
|
|
812
|
+
// spike. This is a direct closed loop: no guessed LAN speed cap.
|
|
813
|
+
if (measuredRate >= st.paceRateBps * LAN_STARTUP_KEEPUP) {
|
|
783
814
|
st.fullBwRounds = 0;
|
|
784
815
|
}
|
|
785
816
|
else {
|
|
786
817
|
st.fullBwRounds++;
|
|
787
818
|
}
|
|
788
|
-
const rawQueue = st.minRttMs !== Infinity ?
|
|
789
|
-
|
|
819
|
+
const rawQueue = st.minRttMs !== Infinity ? acceptedRtt - st.minRttMs : 0;
|
|
820
|
+
const startupOvershot = rawQueue > LAN_STARTUP_QUEUE_MS;
|
|
821
|
+
const startupUnderDelivering = st.fullBwRounds >= LAN_STARTUP_SLOW_ROUNDS;
|
|
822
|
+
if (startupUnderDelivering || startupOvershot) {
|
|
790
823
|
st.bbrPhase = 2;
|
|
791
824
|
}
|
|
792
|
-
if (
|
|
793
|
-
|
|
825
|
+
if (startupUnderDelivering || startupOvershot) {
|
|
826
|
+
// Forget ACK-compressed highs immediately and drain at 80% of
|
|
827
|
+
// the pre-spike pace. Keeping the max-filtered high here made
|
|
828
|
+
// the sender continue flooding for ~2 s and collapse cwnd.
|
|
829
|
+
st.bwSamples = measuredRate > 0 ? [measuredRate] : [];
|
|
830
|
+
st.btlBwBps = measuredRate > 0 ? measuredRate : PACE_INIT_BPS;
|
|
831
|
+
st.paceRateBps = Math.max(PACE_INIT_BPS, Math.min(st.paceRateBps * 0.8, st.btlBwBps * 1.1));
|
|
832
|
+
}
|
|
833
|
+
else if (st.bbrPhase === 0) {
|
|
834
|
+
st.paceRateBps = Math.max(st.paceRateBps, Math.min(st.paceRateBps * LAN_STARTUP_MAX_GAIN, Math.max(PACE_INIT_BPS, measuredRate * LAN_STARTUP_DELIVERY_GAIN)));
|
|
794
835
|
}
|
|
795
836
|
else {
|
|
796
|
-
st.paceRateBps = Math.max(PACE_INIT_BPS * 0.5, st.btlBwBps * lossMult);
|
|
837
|
+
st.paceRateBps = Math.max(PACE_INIT_BPS * 0.5, Math.min(st.paceRateBps, st.btlBwBps * lossMult));
|
|
797
838
|
}
|
|
798
839
|
}
|
|
799
840
|
else {
|
|
@@ -806,9 +847,13 @@ export class FileTransferManager {
|
|
|
806
847
|
st.paceRateBps = Math.min(st.paceRateBps, (WINDOW_CHUNKS * MAX_FILE_DATA_SIZE) / (st.minRttMs / 1000));
|
|
807
848
|
}
|
|
808
849
|
// cwnd = safety ceiling sized to 2× pace BDP. Pacing is primary.
|
|
809
|
-
const
|
|
810
|
-
? (
|
|
811
|
-
|
|
850
|
+
const bdpRttMs = st.minRttMs !== Infinity && st.minRttMs > 0
|
|
851
|
+
? (lanNow ? Math.max(st.minRttMs, LAN_BDP_RTT_FLOOR_MS) : st.minRttMs)
|
|
852
|
+
: 0;
|
|
853
|
+
const paceBdp = bdpRttMs > 0
|
|
854
|
+
? (st.paceRateBps * (bdpRttMs / 1000)) / MAX_FILE_DATA_SIZE : WINDOW_CHUNKS;
|
|
855
|
+
const cwndFloor = lanNow ? CWND_INIT_CHUNKS : CWND_MIN_CHUNKS;
|
|
856
|
+
st.cwnd = Math.max(cwndFloor, Math.min(WINDOW_CHUNKS, Math.ceil(paceBdp * 2)));
|
|
812
857
|
}
|
|
813
858
|
}
|
|
814
859
|
if (process.env.DECENT_DEBUG && nowT - st.lastCcLogMs >= 1000) {
|
|
@@ -816,7 +861,9 @@ export class FileTransferManager {
|
|
|
816
861
|
console.error(`[file-cc] friend=${friendId.slice(0, 8)} ack=${st.acked}/${st.size}` +
|
|
817
862
|
` rtt=${rtt}ms srtt=${st.srttMs.toFixed(1)}ms low=${st.lowRttCount}` +
|
|
818
863
|
` accepted=${(!lowSample || promoteLowPath) ? 1 : 0}` +
|
|
819
|
-
` pace=${Math.round(st.paceRateBps)}Bps
|
|
864
|
+
` pace=${Math.round(st.paceRateBps)}Bps bw=${Math.round(st.btlBwBps)}Bps` +
|
|
865
|
+
` cwnd=${st.cwnd} phase=${st.bbrPhase} slow=${st.fullBwRounds}` +
|
|
866
|
+
` loss=${(st.lossEwma * 100).toFixed(1)}% rounds=${st.roundCount}`);
|
|
820
867
|
}
|
|
821
868
|
}
|
|
822
869
|
if (st.acked - st.lastProgressAcked >= 256 * 1024 || st.acked >= st.size) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.98",
|
|
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",
|