@decentnetwork/peer 0.1.95 → 0.1.97
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 +129 -54
- 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;
|
|
@@ -120,6 +120,7 @@ const PACE_CYCLE_GAINS = [1.25, 0.8, 1, 1, 1, 1, 1, 1]; // steady PROBE_BW cycle
|
|
|
120
120
|
const PACE_FULLBW_GROWTH = 1.20; // bandwidth still "rising" if ≥20% over the plateau mark
|
|
121
121
|
const PACE_FULLBW_ROUNDS = 3; // leave startup after this many non-rising rounds
|
|
122
122
|
const PACE_BW_WINDOW = 10; // rounds of delivered-rate history for the max-filter
|
|
123
|
+
const PACE_SAMPLE_MS = 200; // aggregate ACKs so one scheduler-jittered packet is not a "round"
|
|
123
124
|
const PACE_INIT_BPS = 120_000; // initial pace rate (~120 KB/s) before any bandwidth estimate
|
|
124
125
|
const PACE_BURST_MS = 20; // token-bucket burst allowance (keeps sends smooth, not bunched)
|
|
125
126
|
const WATCHDOG_MS = 150; // stall poll cadence / base no-progress patience
|
|
@@ -195,14 +196,16 @@ const chunkCount = (size) => (size <= 0 ? 0 : Math.ceil(size / MAX_FILE_DATA_SIZ
|
|
|
195
196
|
export class FileTransferManager {
|
|
196
197
|
send;
|
|
197
198
|
emit;
|
|
199
|
+
isLanPath;
|
|
198
200
|
#sending = new Map();
|
|
199
201
|
#receiving = new Map();
|
|
200
202
|
// When set, incoming transfers persist their contiguous prefix here so a
|
|
201
203
|
// dropped/restarted transfer resumes instead of restarting (断点续传).
|
|
202
204
|
#resumeDir;
|
|
203
|
-
constructor(send, emit) {
|
|
205
|
+
constructor(send, emit, isLanPath = () => false) {
|
|
204
206
|
this.send = send;
|
|
205
207
|
this.emit = emit;
|
|
208
|
+
this.isLanPath = isLanPath;
|
|
206
209
|
}
|
|
207
210
|
/** Enable resumable receives: partials are persisted under `dir` and matched
|
|
208
211
|
* on re-offer by content-hash fileId. Unset = in-memory only (no resume). */
|
|
@@ -253,7 +256,8 @@ export class FileTransferManager {
|
|
|
253
256
|
minRttMs: Infinity, minSrttMs: Infinity, srttMs: 0, probeOffset: -1, probeSentMs: 0,
|
|
254
257
|
paceRateBps: PACE_INIT_BPS, paceTokens: 0, lastPaceMs: nowMs(),
|
|
255
258
|
btlBwBps: 0, bwSamples: [], fullBwBps: 0, fullBwRounds: 0, bbrPhase: 0, roundCount: 0,
|
|
256
|
-
rateMarkMs: nowMs(), rateMarkAcked: 0,
|
|
259
|
+
rateMarkMs: nowMs(), rateMarkAcked: 0, lowRttCount: 0, lowRttMinMs: Infinity, lastCcLogMs: 0,
|
|
260
|
+
lanPath: this.isLanPath(friendId),
|
|
257
261
|
};
|
|
258
262
|
this.#map(this.#sending, friendId).set(fileNumber, st);
|
|
259
263
|
void this.send(friendId, PACKET_ID_FILE_SENDREQUEST, req).catch(() => undefined);
|
|
@@ -677,6 +681,11 @@ export class FileTransferManager {
|
|
|
677
681
|
// Progress. The path is delivering → additively re-open the window (up to
|
|
678
682
|
// the cap) and clear the stall streak (patience shrinks back to the base).
|
|
679
683
|
const nowT = nowMs();
|
|
684
|
+
// A receiver may already have a persisted prefix from an earlier UI/CLI
|
|
685
|
+
// attempt. Do not count that old data as bytes delivered by this new
|
|
686
|
+
// connection: doing so creates a fictitious multi-MB/s bandwidth sample
|
|
687
|
+
// and an unsafe startup burst.
|
|
688
|
+
const resumedPrefix = st.acked === 0 && ackedOffset > st.nextSend;
|
|
680
689
|
st.acked = Math.min(ackedOffset, st.size);
|
|
681
690
|
st.lastAckAdvanceMs = nowT;
|
|
682
691
|
st.stalls = 0;
|
|
@@ -685,67 +694,129 @@ export class FileTransferManager {
|
|
|
685
694
|
// instead of re-transmitting them.
|
|
686
695
|
if (st.nextSend < st.acked)
|
|
687
696
|
st.nextSend = st.acked;
|
|
697
|
+
if (resumedPrefix) {
|
|
698
|
+
st.rateMarkMs = nowT;
|
|
699
|
+
st.rateMarkAcked = st.acked;
|
|
700
|
+
st.probeOffset = -1;
|
|
701
|
+
}
|
|
688
702
|
// Vegas: adjust the window ONCE PER RTT, when the timed frontier byte's ack
|
|
689
703
|
// returns (per-ack would fire several times before a slow path's probe
|
|
690
704
|
// reported, overshooting). Estimate chunks queued from RTT inflation and
|
|
691
705
|
// steer to keep it in [ALPHA, BETA].
|
|
692
|
-
if (st.probeOffset >= 0 && st.acked >= st.probeOffset) {
|
|
706
|
+
if (!resumedPrefix && st.probeOffset >= 0 && st.acked >= st.probeOffset) {
|
|
693
707
|
// One paced round completed (the timed frontier byte's ack landed).
|
|
694
708
|
const rtt = nowT - st.probeSentMs;
|
|
695
709
|
st.probeOffset = -1;
|
|
696
|
-
//
|
|
697
|
-
//
|
|
698
|
-
//
|
|
699
|
-
//
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
710
|
+
// A single very-low sample can be a batched/late ACK that makes a probe
|
|
711
|
+
// look newer than it is. But several consecutive low samples mean the
|
|
712
|
+
// path genuinely improved (most importantly tcp-relay → LAN UDP). The
|
|
713
|
+
// old code rejected every such sample against the permanently-high srtt,
|
|
714
|
+
// so startup stayed pinned near 120 KB/s forever. Promote a stable lower
|
|
715
|
+
// RTT after three samples and rebase srtt/RTprop to the new path.
|
|
716
|
+
const lowSample = st.srttMs > 0 && rtt > 0 && rtt < st.srttMs * 0.4;
|
|
717
|
+
if (lowSample) {
|
|
718
|
+
st.lowRttCount++;
|
|
719
|
+
st.lowRttMinMs = Math.min(st.lowRttMinMs, rtt);
|
|
720
|
+
}
|
|
721
|
+
else {
|
|
722
|
+
st.lowRttCount = 0;
|
|
723
|
+
st.lowRttMinMs = Infinity;
|
|
724
|
+
}
|
|
725
|
+
const promoteLowPath = lowSample && st.lowRttCount >= 3;
|
|
726
|
+
const acceptedRtt = promoteLowPath ? st.lowRttMinMs : rtt;
|
|
727
|
+
if (promoteLowPath) {
|
|
728
|
+
st.srttMs = acceptedRtt;
|
|
729
|
+
st.minRttMs = acceptedRtt;
|
|
730
|
+
st.lowRttCount = 0;
|
|
731
|
+
st.lowRttMinMs = Infinity;
|
|
732
|
+
}
|
|
733
|
+
if ((!lowSample || promoteLowPath) && acceptedRtt > 0) {
|
|
703
734
|
// 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
|
-
|
|
735
|
+
st.srttMs = st.srttMs === 0 ? acceptedRtt : 0.85 * st.srttMs + 0.15 * acceptedRtt;
|
|
736
|
+
if (acceptedRtt < st.minRttMs)
|
|
737
|
+
st.minRttMs = acceptedRtt;
|
|
738
|
+
// Aggregate ACKs into a time-sized delivery sample. Previously every
|
|
739
|
+
// single ACK was treated as a BBR round and dt was clamped to 50 ms;
|
|
740
|
+
// on a 1–2 ms LAN that made the measured rate look permanently tiny
|
|
741
|
+
// and normal scheduler jitter immediately ended STARTUP.
|
|
742
|
+
const sampleMs = nowT - st.rateMarkMs;
|
|
743
|
+
if (sampleMs >= PACE_SAMPLE_MS) {
|
|
744
|
+
st.roundCount++;
|
|
745
|
+
const roundRate = ((st.acked - st.rateMarkAcked) * 1000) / sampleMs;
|
|
746
|
+
st.rateMarkMs = nowT;
|
|
747
|
+
st.rateMarkAcked = st.acked;
|
|
748
|
+
if (roundRate > 0) {
|
|
749
|
+
st.bwSamples.push(roundRate);
|
|
750
|
+
if (st.bwSamples.length > PACE_BW_WINDOW)
|
|
751
|
+
st.bwSamples.shift();
|
|
752
|
+
st.btlBwBps = Math.max(...st.bwSamples);
|
|
753
|
+
}
|
|
754
|
+
// Wire carries goodput + FEC parity + retransmits, so to fill the LINK
|
|
755
|
+
// we pace above goodput by the loss overhead: wire = goodput/(1 − loss).
|
|
756
|
+
const lossMult = 1 / (1 - Math.min(0.5, st.lossEwma));
|
|
757
|
+
const lanNow = this.isLanPath(friendId);
|
|
758
|
+
if (lanNow && !st.lanPath) {
|
|
759
|
+
// A transfer can begin on relay and then promote to a validated
|
|
760
|
+
// physical-LAN endpoint. Restart STARTUP from the measured rate.
|
|
761
|
+
st.bbrPhase = 0;
|
|
762
|
+
st.fullBwBps = 0;
|
|
763
|
+
st.fullBwRounds = 0;
|
|
764
|
+
}
|
|
765
|
+
st.lanPath = lanNow;
|
|
766
|
+
if (!lanNow) {
|
|
767
|
+
// Relay/public-NAT file traffic must remain conservative. A short
|
|
768
|
+
// ACK burst can wildly overstate bandwidth there; allowing LAN's
|
|
769
|
+
// exponential STARTUP filled the TCP relay with an 8.7 s queue.
|
|
770
|
+
// This affects only file DATA/FEC — IP/video has its own path.
|
|
732
771
|
st.bbrPhase = 2;
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
772
|
+
const gain = PACE_CYCLE_GAINS[st.roundCount % PACE_CYCLE_GAINS.length];
|
|
773
|
+
st.paceRateBps = Math.max(PACE_INIT_BPS, Math.min(PACE_INIT_BPS * 2, st.btlBwBps * gain * lossMult));
|
|
774
|
+
}
|
|
775
|
+
else if (st.bbrPhase === 0) {
|
|
776
|
+
// BBR full-bandwidth detection: remain in STARTUP while delivered
|
|
777
|
+
// bandwidth grows. A relative RTT threshold is unusable on LAN —
|
|
778
|
+
// 1 ms → 2 ms scheduler jitter looked like a 100% queue and pinned
|
|
779
|
+
// every transfer near PACE_INIT_BPS. The absolute, smoothed queue
|
|
780
|
+
// ceiling still protects a deep relay from runaway buffering.
|
|
781
|
+
if (st.fullBwBps === 0 || st.btlBwBps >= st.fullBwBps * PACE_FULLBW_GROWTH) {
|
|
782
|
+
st.fullBwBps = st.btlBwBps;
|
|
783
|
+
st.fullBwRounds = 0;
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
st.fullBwRounds++;
|
|
787
|
+
}
|
|
788
|
+
const rawQueue = st.minRttMs !== Infinity ? st.srttMs - st.minRttMs : 0;
|
|
789
|
+
if (st.fullBwRounds >= PACE_FULLBW_ROUNDS || rawQueue > MAX_QUEUE_MS) {
|
|
790
|
+
st.bbrPhase = 2;
|
|
791
|
+
}
|
|
792
|
+
if (st.bbrPhase === 0) {
|
|
793
|
+
st.paceRateBps = Math.max(st.paceRateBps * 1.4, st.btlBwBps * PACE_STARTUP_GAIN * lossMult);
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
st.paceRateBps = Math.max(PACE_INIT_BPS * 0.5, st.btlBwBps * lossMult);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
// PROBE_BW cruise: one sample probes up, one drains, six cruise.
|
|
801
|
+
const gain = PACE_CYCLE_GAINS[st.roundCount % PACE_CYCLE_GAINS.length];
|
|
802
|
+
st.paceRateBps = Math.max(PACE_INIT_BPS * 0.5, st.btlBwBps * gain * lossMult);
|
|
803
|
+
}
|
|
804
|
+
// Hard safety cap: never pace faster than a full window drains in 1 RTT.
|
|
805
|
+
if (st.minRttMs !== Infinity && st.minRttMs > 0) {
|
|
806
|
+
st.paceRateBps = Math.min(st.paceRateBps, (WINDOW_CHUNKS * MAX_FILE_DATA_SIZE) / (st.minRttMs / 1000));
|
|
807
|
+
}
|
|
808
|
+
// cwnd = safety ceiling sized to 2× pace BDP. Pacing is primary.
|
|
809
|
+
const paceBdp = (st.minRttMs !== Infinity && st.minRttMs > 0)
|
|
810
|
+
? (st.paceRateBps * (st.minRttMs / 1000)) / MAX_FILE_DATA_SIZE : WINDOW_CHUNKS;
|
|
811
|
+
st.cwnd = Math.max(CWND_MIN_CHUNKS, Math.min(WINDOW_CHUNKS, Math.ceil(paceBdp * 2)));
|
|
743
812
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
813
|
+
}
|
|
814
|
+
if (process.env.DECENT_DEBUG && nowT - st.lastCcLogMs >= 1000) {
|
|
815
|
+
st.lastCcLogMs = nowT;
|
|
816
|
+
console.error(`[file-cc] friend=${friendId.slice(0, 8)} ack=${st.acked}/${st.size}` +
|
|
817
|
+
` rtt=${rtt}ms srtt=${st.srttMs.toFixed(1)}ms low=${st.lowRttCount}` +
|
|
818
|
+
` accepted=${(!lowSample || promoteLowPath) ? 1 : 0}` +
|
|
819
|
+
` pace=${Math.round(st.paceRateBps)}Bps cwnd=${st.cwnd} rounds=${st.roundCount}`);
|
|
749
820
|
}
|
|
750
821
|
}
|
|
751
822
|
if (st.acked - st.lastProgressAcked >= 256 * 1024 || st.acked >= st.size) {
|
|
@@ -861,7 +932,11 @@ export class FileTransferManager {
|
|
|
861
932
|
try {
|
|
862
933
|
await this.send(friendId, PACKET_ID_FILE_DATA, encodeFileData(st.fileNumber, off, st.data.subarray(off, end)));
|
|
863
934
|
}
|
|
864
|
-
catch {
|
|
935
|
+
catch (error) {
|
|
936
|
+
if (process.env.DECENT_DEBUG || process.env.DECENT_DEBUG_VERBOSE) {
|
|
937
|
+
console.error(`[file-ft] data-send-error friend=${friendId.slice(0, 8)} num=${st.fileNumber} ` +
|
|
938
|
+
`off=${off} error=${error instanceof Error ? error.message : String(error)}`);
|
|
939
|
+
}
|
|
865
940
|
break; // transport down; the watchdog re-kicks
|
|
866
941
|
}
|
|
867
942
|
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.97",
|
|
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",
|