@decentnetwork/peer 0.1.95 → 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.d.ts +3 -1
- package/dist/compat/filetransfer.js +207 -85
- package/dist/peer.js +39 -29
- package/package.json +1 -1
|
@@ -40,11 +40,13 @@ export declare function parseFileData(p: Uint8Array): {
|
|
|
40
40
|
} | undefined;
|
|
41
41
|
export type FtSend = (friendId: string, packetId: number, payload: Uint8Array) => Promise<unknown>;
|
|
42
42
|
export type FtEmit = (event: string, payload: Record<string, unknown>) => void;
|
|
43
|
+
export type FtIsLanPath = (friendId: string) => boolean;
|
|
43
44
|
export declare class FileTransferManager {
|
|
44
45
|
#private;
|
|
45
46
|
private readonly send;
|
|
46
47
|
private readonly emit;
|
|
47
|
-
|
|
48
|
+
private readonly isLanPath;
|
|
49
|
+
constructor(send: FtSend, emit: FtEmit, isLanPath?: FtIsLanPath);
|
|
48
50
|
/** Enable resumable receives: partials are persisted under `dir` and matched
|
|
49
51
|
* on re-offer by content-hash fileId. Unset = in-memory only (no resume). */
|
|
50
52
|
setResumeDir(dir: string): void;
|
|
@@ -86,47 +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
|
|
97
|
+
const PACE_SAMPLE_MS = 200; // aggregate ACKs so one scheduler-jittered packet is not a "round"
|
|
123
98
|
const PACE_INIT_BPS = 120_000; // initial pace rate (~120 KB/s) before any bandwidth estimate
|
|
124
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;
|
|
125
120
|
const WATCHDOG_MS = 150; // stall poll cadence / base no-progress patience
|
|
126
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)
|
|
127
122
|
const FAST_RT_MIN_MS = 40; // min gap between fast-retransmits of the same hole (≈1 RTT)
|
|
128
123
|
const ACK_THROTTLE_MS = 15; // coalesce receiver acks (fast enough to drive fast-retransmit)
|
|
129
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
|
|
130
126
|
const SEND_GIVEUP_MS = 60000; // abandon a send after this long with no ack progress (peer gone)
|
|
131
127
|
const PERSIST_INTERVAL_BYTES = 1024 * 1024; // flush the resume file at most once per this much progress
|
|
132
128
|
function u32be(n) {
|
|
@@ -195,14 +191,16 @@ const chunkCount = (size) => (size <= 0 ? 0 : Math.ceil(size / MAX_FILE_DATA_SIZ
|
|
|
195
191
|
export class FileTransferManager {
|
|
196
192
|
send;
|
|
197
193
|
emit;
|
|
194
|
+
isLanPath;
|
|
198
195
|
#sending = new Map();
|
|
199
196
|
#receiving = new Map();
|
|
200
197
|
// When set, incoming transfers persist their contiguous prefix here so a
|
|
201
198
|
// dropped/restarted transfer resumes instead of restarting (断点续传).
|
|
202
199
|
#resumeDir;
|
|
203
|
-
constructor(send, emit) {
|
|
200
|
+
constructor(send, emit, isLanPath = () => false) {
|
|
204
201
|
this.send = send;
|
|
205
202
|
this.emit = emit;
|
|
203
|
+
this.isLanPath = isLanPath;
|
|
206
204
|
}
|
|
207
205
|
/** Enable resumable receives: partials are persisted under `dir` and matched
|
|
208
206
|
* on re-offer by content-hash fileId. Unset = in-memory only (no resume). */
|
|
@@ -250,10 +248,11 @@ export class FileTransferManager {
|
|
|
250
248
|
acked: 0, nextSend: 0, pumping: false, started: false, req, startMs: nowMs(),
|
|
251
249
|
lastProgressAcked: 0, lastAckAdvanceMs: nowMs(), lastResendMs: 0,
|
|
252
250
|
cwnd: CWND_INIT_CHUNKS, stalls: 0, parityHighBlock: -1, lastLossMs: 0, lossEwma: 0,
|
|
253
|
-
minRttMs: Infinity,
|
|
251
|
+
minRttMs: Infinity, srttMs: 0, probeOffset: -1, probeSentMs: 0,
|
|
254
252
|
paceRateBps: PACE_INIT_BPS, paceTokens: 0, lastPaceMs: nowMs(),
|
|
255
|
-
btlBwBps: 0, bwSamples: [],
|
|
256
|
-
rateMarkMs: nowMs(), rateMarkAcked: 0,
|
|
253
|
+
btlBwBps: 0, bwSamples: [], fullBwRounds: 0, bbrPhase: 0, roundCount: 0,
|
|
254
|
+
rateMarkMs: nowMs(), rateMarkAcked: 0, lowRttCount: 0, lowRttMinMs: Infinity, lastCcLogMs: 0,
|
|
255
|
+
lanPath: this.isLanPath(friendId),
|
|
257
256
|
};
|
|
258
257
|
this.#map(this.#sending, friendId).set(fileNumber, st);
|
|
259
258
|
void this.send(friendId, PACKET_ID_FILE_SENDREQUEST, req).catch(() => undefined);
|
|
@@ -595,7 +594,25 @@ export class FileTransferManager {
|
|
|
595
594
|
clearInterval(st.reackTimer);
|
|
596
595
|
st.reackTimer = undefined;
|
|
597
596
|
}
|
|
598
|
-
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();
|
|
599
616
|
const data = st.buf;
|
|
600
617
|
// Transfer complete → the resume file is no longer needed.
|
|
601
618
|
if (st.partPath) {
|
|
@@ -612,9 +629,14 @@ export class FileTransferManager {
|
|
|
612
629
|
st.got = new Uint8Array(0);
|
|
613
630
|
const t = setTimeout(() => {
|
|
614
631
|
const cur = this.#map(this.#receiving, friendId).get(st.fileNumber);
|
|
615
|
-
if (cur === st)
|
|
632
|
+
if (cur === st) {
|
|
633
|
+
if (st.reackTimer) {
|
|
634
|
+
clearInterval(st.reackTimer);
|
|
635
|
+
st.reackTimer = undefined;
|
|
636
|
+
}
|
|
616
637
|
this.#map(this.#receiving, friendId).delete(st.fileNumber);
|
|
617
|
-
|
|
638
|
+
}
|
|
639
|
+
}, FINAL_ACK_GRACE_MS);
|
|
618
640
|
if (typeof t.unref === "function")
|
|
619
641
|
t.unref();
|
|
620
642
|
}
|
|
@@ -677,6 +699,11 @@ export class FileTransferManager {
|
|
|
677
699
|
// Progress. The path is delivering → additively re-open the window (up to
|
|
678
700
|
// the cap) and clear the stall streak (patience shrinks back to the base).
|
|
679
701
|
const nowT = nowMs();
|
|
702
|
+
// A receiver may already have a persisted prefix from an earlier UI/CLI
|
|
703
|
+
// attempt. Do not count that old data as bytes delivered by this new
|
|
704
|
+
// connection: doing so creates a fictitious multi-MB/s bandwidth sample
|
|
705
|
+
// and an unsafe startup burst.
|
|
706
|
+
const resumedPrefix = st.acked === 0 && ackedOffset > st.nextSend;
|
|
680
707
|
st.acked = Math.min(ackedOffset, st.size);
|
|
681
708
|
st.lastAckAdvanceMs = nowT;
|
|
682
709
|
st.stalls = 0;
|
|
@@ -685,67 +712,158 @@ export class FileTransferManager {
|
|
|
685
712
|
// instead of re-transmitting them.
|
|
686
713
|
if (st.nextSend < st.acked)
|
|
687
714
|
st.nextSend = st.acked;
|
|
715
|
+
if (resumedPrefix) {
|
|
716
|
+
st.rateMarkMs = nowT;
|
|
717
|
+
st.rateMarkAcked = st.acked;
|
|
718
|
+
st.probeOffset = -1;
|
|
719
|
+
}
|
|
688
720
|
// Vegas: adjust the window ONCE PER RTT, when the timed frontier byte's ack
|
|
689
721
|
// returns (per-ack would fire several times before a slow path's probe
|
|
690
722
|
// reported, overshooting). Estimate chunks queued from RTT inflation and
|
|
691
723
|
// steer to keep it in [ALPHA, BETA].
|
|
692
|
-
if (st.probeOffset >= 0 && st.acked >= st.probeOffset) {
|
|
724
|
+
if (!resumedPrefix && st.probeOffset >= 0 && st.acked >= st.probeOffset) {
|
|
693
725
|
// One paced round completed (the timed frontier byte's ack landed).
|
|
694
726
|
const rtt = nowT - st.probeSentMs;
|
|
695
727
|
st.probeOffset = -1;
|
|
696
|
-
//
|
|
697
|
-
//
|
|
698
|
-
//
|
|
699
|
-
//
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
728
|
+
// A single very-low sample can be a batched/late ACK that makes a probe
|
|
729
|
+
// look newer than it is. But several consecutive low samples mean the
|
|
730
|
+
// path genuinely improved (most importantly tcp-relay → LAN UDP). The
|
|
731
|
+
// old code rejected every such sample against the permanently-high srtt,
|
|
732
|
+
// so startup stayed pinned near 120 KB/s forever. Promote a stable lower
|
|
733
|
+
// RTT after three samples and rebase srtt/RTprop to the new path.
|
|
734
|
+
const lowSample = st.srttMs > 0 && rtt > 0 && rtt < st.srttMs * 0.4;
|
|
735
|
+
if (lowSample) {
|
|
736
|
+
st.lowRttCount++;
|
|
737
|
+
st.lowRttMinMs = Math.min(st.lowRttMinMs, rtt);
|
|
738
|
+
}
|
|
739
|
+
else {
|
|
740
|
+
st.lowRttCount = 0;
|
|
741
|
+
st.lowRttMinMs = Infinity;
|
|
742
|
+
}
|
|
743
|
+
const promoteLowPath = lowSample && st.lowRttCount >= 3;
|
|
744
|
+
const acceptedRtt = promoteLowPath ? st.lowRttMinMs : rtt;
|
|
745
|
+
if (promoteLowPath) {
|
|
746
|
+
st.srttMs = acceptedRtt;
|
|
747
|
+
st.minRttMs = acceptedRtt;
|
|
748
|
+
st.lowRttCount = 0;
|
|
749
|
+
st.lowRttMinMs = Infinity;
|
|
750
|
+
}
|
|
751
|
+
if ((!lowSample || promoteLowPath) && acceptedRtt > 0) {
|
|
703
752
|
// RTprop = min RTT ever seen (loss/queue only RAISE rtt → min is robust).
|
|
704
|
-
st.srttMs = st.srttMs === 0 ?
|
|
705
|
-
if (
|
|
706
|
-
st.minRttMs =
|
|
707
|
-
//
|
|
708
|
-
//
|
|
709
|
-
//
|
|
710
|
-
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
st.
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
753
|
+
st.srttMs = st.srttMs === 0 ? acceptedRtt : 0.85 * st.srttMs + 0.15 * acceptedRtt;
|
|
754
|
+
if (acceptedRtt < st.minRttMs)
|
|
755
|
+
st.minRttMs = acceptedRtt;
|
|
756
|
+
// Aggregate ACKs into a time-sized delivery sample. Previously every
|
|
757
|
+
// single ACK was treated as a BBR round and dt was clamped to 50 ms;
|
|
758
|
+
// on a 1–2 ms LAN that made the measured rate look permanently tiny
|
|
759
|
+
// and normal scheduler jitter immediately ended STARTUP.
|
|
760
|
+
const sampleMs = nowT - st.rateMarkMs;
|
|
761
|
+
if (sampleMs >= PACE_SAMPLE_MS) {
|
|
762
|
+
st.roundCount++;
|
|
763
|
+
const roundRate = ((st.acked - st.rateMarkAcked) * 1000) / sampleMs;
|
|
764
|
+
st.rateMarkMs = nowT;
|
|
765
|
+
st.rateMarkAcked = st.acked;
|
|
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);
|
|
776
|
+
if (st.bwSamples.length > PACE_BW_WINDOW)
|
|
777
|
+
st.bwSamples.shift();
|
|
778
|
+
st.btlBwBps = Math.max(...st.bwSamples);
|
|
779
|
+
}
|
|
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));
|
|
785
|
+
if (lanNow && !st.lanPath) {
|
|
786
|
+
// A transfer can begin on relay and then promote to a validated
|
|
787
|
+
// physical-LAN endpoint. Restart STARTUP from the measured rate.
|
|
788
|
+
st.bbrPhase = 0;
|
|
789
|
+
st.fullBwRounds = 0;
|
|
790
|
+
}
|
|
791
|
+
st.lanPath = lanNow;
|
|
792
|
+
if (!lanNow) {
|
|
793
|
+
// Relay/public-NAT file traffic must remain conservative. A short
|
|
794
|
+
// ACK burst can wildly overstate bandwidth there; allowing LAN's
|
|
795
|
+
// exponential STARTUP filled the TCP relay with an 8.7 s queue.
|
|
796
|
+
// This affects only file DATA/FEC — IP/video has its own path.
|
|
732
797
|
st.bbrPhase = 2;
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
798
|
+
const gain = PACE_CYCLE_GAINS[st.roundCount % PACE_CYCLE_GAINS.length];
|
|
799
|
+
st.paceRateBps = Math.max(PACE_INIT_BPS, Math.min(PACE_INIT_BPS * 2, st.btlBwBps * gain * lossMult));
|
|
800
|
+
}
|
|
801
|
+
else if (st.bbrPhase === 0) {
|
|
802
|
+
// BBR full-bandwidth detection: remain in STARTUP while delivered
|
|
803
|
+
// bandwidth grows. A relative RTT threshold is unusable on LAN —
|
|
804
|
+
// 1 ms → 2 ms scheduler jitter looked like a 100% queue and pinned
|
|
805
|
+
// every transfer near PACE_INIT_BPS. The absolute, smoothed queue
|
|
806
|
+
// ceiling still protects a deep relay from runaway buffering.
|
|
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) {
|
|
814
|
+
st.fullBwRounds = 0;
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
st.fullBwRounds++;
|
|
818
|
+
}
|
|
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) {
|
|
823
|
+
st.bbrPhase = 2;
|
|
824
|
+
}
|
|
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)));
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
837
|
+
st.paceRateBps = Math.max(PACE_INIT_BPS * 0.5, Math.min(st.paceRateBps, st.btlBwBps * lossMult));
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
// PROBE_BW cruise: one sample probes up, one drains, six cruise.
|
|
842
|
+
const gain = PACE_CYCLE_GAINS[st.roundCount % PACE_CYCLE_GAINS.length];
|
|
843
|
+
st.paceRateBps = Math.max(PACE_INIT_BPS * 0.5, st.btlBwBps * gain * lossMult);
|
|
844
|
+
}
|
|
845
|
+
// Hard safety cap: never pace faster than a full window drains in 1 RTT.
|
|
846
|
+
if (st.minRttMs !== Infinity && st.minRttMs > 0) {
|
|
847
|
+
st.paceRateBps = Math.min(st.paceRateBps, (WINDOW_CHUNKS * MAX_FILE_DATA_SIZE) / (st.minRttMs / 1000));
|
|
848
|
+
}
|
|
849
|
+
// cwnd = safety ceiling sized to 2× pace BDP. Pacing is primary.
|
|
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)));
|
|
743
857
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
858
|
+
}
|
|
859
|
+
if (process.env.DECENT_DEBUG && nowT - st.lastCcLogMs >= 1000) {
|
|
860
|
+
st.lastCcLogMs = nowT;
|
|
861
|
+
console.error(`[file-cc] friend=${friendId.slice(0, 8)} ack=${st.acked}/${st.size}` +
|
|
862
|
+
` rtt=${rtt}ms srtt=${st.srttMs.toFixed(1)}ms low=${st.lowRttCount}` +
|
|
863
|
+
` accepted=${(!lowSample || promoteLowPath) ? 1 : 0}` +
|
|
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}`);
|
|
749
867
|
}
|
|
750
868
|
}
|
|
751
869
|
if (st.acked - st.lastProgressAcked >= 256 * 1024 || st.acked >= st.size) {
|
|
@@ -861,7 +979,11 @@ export class FileTransferManager {
|
|
|
861
979
|
try {
|
|
862
980
|
await this.send(friendId, PACKET_ID_FILE_DATA, encodeFileData(st.fileNumber, off, st.data.subarray(off, end)));
|
|
863
981
|
}
|
|
864
|
-
catch {
|
|
982
|
+
catch (error) {
|
|
983
|
+
if (process.env.DECENT_DEBUG || process.env.DECENT_DEBUG_VERBOSE) {
|
|
984
|
+
console.error(`[file-ft] data-send-error friend=${friendId.slice(0, 8)} num=${st.fileNumber} ` +
|
|
985
|
+
`off=${off} error=${error instanceof Error ? error.message : String(error)}`);
|
|
986
|
+
}
|
|
865
987
|
break; // transport down; the watchdog re-kicks
|
|
866
988
|
}
|
|
867
989
|
st.paceTokens -= end - off;
|
package/dist/peer.js
CHANGED
|
@@ -211,7 +211,10 @@ export class Peer {
|
|
|
211
211
|
#opts;
|
|
212
212
|
#events = new EventEmitter();
|
|
213
213
|
// Toxcore-standard file transfer (wire-compatible with native Carrier/toxcore).
|
|
214
|
-
#fileTransfer = new FileTransferManager((friendId, packetId, payload) => this.#sendMessengerPacket(friendId, packetId, payload), (event, payload) => { this.#events.emit(event, payload); })
|
|
214
|
+
#fileTransfer = new FileTransferManager((friendId, packetId, payload) => this.#sendMessengerPacket(friendId, packetId, payload), (event, payload) => { this.#events.emit(event, payload); }, (friendId) => {
|
|
215
|
+
const session = this.#friendSessions.get(friendId);
|
|
216
|
+
return !!session?.lanRemoteHost && session.remote?.host === session.lanRemoteHost;
|
|
217
|
+
});
|
|
215
218
|
#keyPair;
|
|
216
219
|
#udp = new UdpTransport();
|
|
217
220
|
// TURN relay (stable fallback path). One node-level allocation on its own
|
|
@@ -2063,17 +2066,14 @@ export class Peer {
|
|
|
2063
2066
|
// low-water mark, like toxcore net_crypto drops below buffer_start.
|
|
2064
2067
|
//
|
|
2065
2068
|
// EXCEPT single-transport channels (bulk IP = bulkDataPacketId, and
|
|
2066
|
-
// file
|
|
2067
|
-
// duplicates — and the low-water mark would then only drop a
|
|
2068
|
-
// legitimately REORDERED packet
|
|
2069
|
-
//
|
|
2070
|
-
//
|
|
2071
|
-
//
|
|
2072
|
-
// advance so a real duplicate doesn't desync the receive nonce.
|
|
2069
|
+
// file DATA/FEC): they are NOT dual-sent, so they never produce
|
|
2070
|
+
// transport duplicates — and the low-water mark would then only drop a
|
|
2071
|
+
// legitimately REORDERED packet. File SENDREQUEST/CONTROL are sparse
|
|
2072
|
+
// control traffic and intentionally remain dual-sent so they can confirm
|
|
2073
|
+
// an optimistic UDP path before the bulk data pump begins. Must run
|
|
2074
|
+
// BEFORE the nonce advance so a real duplicate doesn't desync it.
|
|
2073
2075
|
const innerKind = opened.payload.length > 0 ? opened.payload[0] : -1;
|
|
2074
2076
|
const isSingleTransportKind = (this.#opts.bulkDataPacketId !== undefined && innerKind === this.#opts.bulkDataPacketId) ||
|
|
2075
|
-
innerKind === PACKET_ID_FILE_SENDREQUEST ||
|
|
2076
|
-
innerKind === PACKET_ID_FILE_CONTROL ||
|
|
2077
2077
|
innerKind === PACKET_ID_FILE_DATA ||
|
|
2078
2078
|
innerKind === PACKET_ID_FILE_FEC;
|
|
2079
2079
|
// Confirm the transport path + liveness for EVERY decrypted packet —
|
|
@@ -3818,7 +3818,15 @@ export class Peer {
|
|
|
3818
3818
|
// bulk-IP channel and lossy packets; the crypto nonce still advances above so
|
|
3819
3819
|
// decryption stays in sync regardless.
|
|
3820
3820
|
const isFileTransfer = kind === PACKET_ID_FILE_SENDREQUEST || kind === PACKET_ID_FILE_CONTROL || kind === PACKET_ID_FILE_DATA || kind === PACKET_ID_FILE_FEC;
|
|
3821
|
-
|
|
3821
|
+
// Offers and control/ACK packets are sparse path-establishment traffic, not
|
|
3822
|
+
// bulk. Dual-send them over an optimistic UDP candidate plus the reliable
|
|
3823
|
+
// relay copy, just like chat/control traffic. A direct FILE_SENDREQUEST or
|
|
3824
|
+
// FILE_CONTROL arrival confirms UDP before FILE_DATA starts, so the data
|
|
3825
|
+
// stream can select the LAN path instead of remaining pinned to tcp-relay.
|
|
3826
|
+
// If UDP is not viable, the relay copy still completes the negotiation and
|
|
3827
|
+
// FILE_DATA falls back to its existing reliable single-transport route.
|
|
3828
|
+
const isFileBulk = kind === PACKET_ID_FILE_DATA || kind === PACKET_ID_FILE_FEC;
|
|
3829
|
+
const isBulkData = (this.#opts.bulkDataPacketId !== undefined && kind === this.#opts.bulkDataPacketId) || isFileBulk;
|
|
3822
3830
|
const isDroppable = (isBulkData && !isFileTransfer) || kind >= 192;
|
|
3823
3831
|
if (!isDroppable) {
|
|
3824
3832
|
session.sendPacketNumber = (packetNumber + 1) >>> 0;
|
|
@@ -3859,20 +3867,12 @@ export class Peer {
|
|
|
3859
3867
|
// e.g. decentlan IP=163) stays bulk / single-transport to avoid the relay
|
|
3860
3868
|
// backlog + duplicate fan-out that high packet rates cause. Chat is sparse
|
|
3861
3869
|
// and net_crypto dedups by packet number, so dual-send is safe here.
|
|
3862
|
-
// File
|
|
3863
|
-
//
|
|
3864
|
-
//
|
|
3865
|
-
//
|
|
3866
|
-
//
|
|
3867
|
-
//
|
|
3868
|
-
// wrong order and never completes, while a tiny one (png, 1-2 chunks)
|
|
3869
|
-
// happens to finish before reordering bites. Pinning file transfer to a
|
|
3870
|
-
// single transport keeps the chunks in send order. (Reliability across a
|
|
3871
|
-
// lossy path still wants a reorder buffer + retransmit — tracked separately.)
|
|
3872
|
-
// File transfer rides the relay RELIABLY (never dropped under backpressure):
|
|
3873
|
-
// a dropped IP/CCTV packet is fine (the stream moves on), but a dropped file
|
|
3874
|
-
// chunk corrupts the file (no retransmit). So bulk IP data stays droppable
|
|
3875
|
-
// while file transfer is queued. (isBulkData / isFileTransfer computed above.)
|
|
3870
|
+
// File DATA/FEC is bulk and stays single-transport. SENDREQUEST and CONTROL
|
|
3871
|
+
// (accept/ACK/kill) are low-volume control traffic: dual-sending those is
|
|
3872
|
+
// what confirms an optimistic LAN candidate before the data pump starts.
|
|
3873
|
+
// File packets ride the relay RELIABLY (never dropped under backpressure):
|
|
3874
|
+
// a dropped IP/CCTV packet is fine, but a dropped file chunk can stall the
|
|
3875
|
+
// transfer. (isBulkData / isFileTransfer computed above.)
|
|
3876
3876
|
await this.#sendToFriend(friendId, encrypted, session, isBulkData, isFileTransfer);
|
|
3877
3877
|
}
|
|
3878
3878
|
/**
|
|
@@ -3896,6 +3896,15 @@ export class Peer {
|
|
|
3896
3896
|
const udpFresh = !!realUdpRemote &&
|
|
3897
3897
|
s?.lastUdpRecvMs !== undefined &&
|
|
3898
3898
|
Date.now() - s.lastUdpRecvMs < 4_000;
|
|
3899
|
+
// A freshly-received UDP packet proves only the peer -> us direction.
|
|
3900
|
+
// That is enough for lossy IP/video bulk, whose inner TCP stream can
|
|
3901
|
+
// recover a dropped packet, but not for reliable file data: returning
|
|
3902
|
+
// UDP-only on a one-way public/NAT endpoint silently black-holes every
|
|
3903
|
+
// chunk. A physical-LAN lock is stronger evidence because the candidate
|
|
3904
|
+
// was validated against the local subnet; otherwise file bulk continues
|
|
3905
|
+
// to TURN/TCP below.
|
|
3906
|
+
const fileUdpOnlyConfirmed = !reliableOnRelay ||
|
|
3907
|
+
(!!s?.lanRemoteHost && s.remote?.host === s.lanRemoteHost);
|
|
3899
3908
|
// Bulk IP data rides the direct path ONLY while it's fresh. When it
|
|
3900
3909
|
// goes stale (likely a NAT remap mid-stream), we do NOT keep firing
|
|
3901
3910
|
// into the dead endpoint and we do NOT duplicate onto UDP — we bridge
|
|
@@ -3903,7 +3912,8 @@ export class Peer {
|
|
|
3903
3912
|
// lastUdpRecvMs). This converts a multi-second blackout into a few
|
|
3904
3913
|
// seconds of higher relay latency. Control packets always probe UDP
|
|
3905
3914
|
// (to keep the NAT mapping warm) so we still try UDP for them.
|
|
3906
|
-
const tryUdp = realUdpRemote &&
|
|
3915
|
+
const tryUdp = realUdpRemote &&
|
|
3916
|
+
(isBulkData ? udpFresh && fileUdpOnlyConfirmed : true);
|
|
3907
3917
|
if (tryUdp && s?.remote) {
|
|
3908
3918
|
try {
|
|
3909
3919
|
await this.#sendPacket(packet, s.remote);
|
|
@@ -3913,9 +3923,9 @@ export class Peer {
|
|
|
3913
3923
|
firstError = error;
|
|
3914
3924
|
}
|
|
3915
3925
|
}
|
|
3916
|
-
// Fresh direct path → send over UDP only
|
|
3917
|
-
//
|
|
3918
|
-
//
|
|
3926
|
+
// Fresh confirmed direct path → send over UDP only, but ONLY for bulk.
|
|
3927
|
+
// For files, `tryUdp` above requires the physical-LAN lock; a merely-fresh
|
|
3928
|
+
// public endpoint deliberately falls through to reliable TURN/TCP.
|
|
3919
3929
|
//
|
|
3920
3930
|
// Low-volume traffic (chat 64 + control keepalives / endpoint offers /
|
|
3921
3931
|
// profile) instead ALSO goes over the relay even when the direct path
|
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",
|