@decentnetwork/peer 0.1.94 → 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 +85 -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
|
|
@@ -358,6 +361,8 @@ export class Peer {
|
|
|
358
361
|
// Per-friend tracking sets so we send our nickname/status/greeting once
|
|
359
362
|
// per session rather than on every PACKET_ID_ONLINE arrival.
|
|
360
363
|
#profileSentTo = new Set();
|
|
364
|
+
#profileRetryAttempts = new Map();
|
|
365
|
+
#profileRetryTimers = new Map();
|
|
361
366
|
#greetingSentTo = new Set();
|
|
362
367
|
#selfAnnouncePromise;
|
|
363
368
|
#selfAnnouncePauseDepth = 0;
|
|
@@ -595,6 +600,10 @@ export class Peer {
|
|
|
595
600
|
async stop() {
|
|
596
601
|
// Stop background producers first so no keepalive/profile packet can race
|
|
597
602
|
// behind the shutdown notice and make the remote session look live again.
|
|
603
|
+
for (const timer of this.#profileRetryTimers.values())
|
|
604
|
+
clearTimeout(timer);
|
|
605
|
+
this.#profileRetryTimers.clear();
|
|
606
|
+
this.#profileRetryAttempts.clear();
|
|
598
607
|
if (this.#expressPollTimer) {
|
|
599
608
|
clearInterval(this.#expressPollTimer);
|
|
600
609
|
this.#expressPollTimer = undefined;
|
|
@@ -981,6 +990,11 @@ export class Peer {
|
|
|
981
990
|
this.#tcpOnlyWarningShown.delete(friendId);
|
|
982
991
|
this.#noEndpointWarned.delete(friendId);
|
|
983
992
|
this.#profileSentTo.delete(friendId);
|
|
993
|
+
const profileRetry = this.#profileRetryTimers.get(friendId);
|
|
994
|
+
if (profileRetry)
|
|
995
|
+
clearTimeout(profileRetry);
|
|
996
|
+
this.#profileRetryTimers.delete(friendId);
|
|
997
|
+
this.#profileRetryAttempts.delete(friendId);
|
|
984
998
|
this.#greetingSentTo.delete(friendId);
|
|
985
999
|
if (existed) {
|
|
986
1000
|
this.#persistFriends();
|
|
@@ -2052,17 +2066,14 @@ export class Peer {
|
|
|
2052
2066
|
// low-water mark, like toxcore net_crypto drops below buffer_start.
|
|
2053
2067
|
//
|
|
2054
2068
|
// EXCEPT single-transport channels (bulk IP = bulkDataPacketId, and
|
|
2055
|
-
// file
|
|
2056
|
-
// duplicates — and the low-water mark would then only drop a
|
|
2057
|
-
// legitimately REORDERED packet
|
|
2058
|
-
//
|
|
2059
|
-
//
|
|
2060
|
-
//
|
|
2061
|
-
// 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.
|
|
2062
2075
|
const innerKind = opened.payload.length > 0 ? opened.payload[0] : -1;
|
|
2063
2076
|
const isSingleTransportKind = (this.#opts.bulkDataPacketId !== undefined && innerKind === this.#opts.bulkDataPacketId) ||
|
|
2064
|
-
innerKind === PACKET_ID_FILE_SENDREQUEST ||
|
|
2065
|
-
innerKind === PACKET_ID_FILE_CONTROL ||
|
|
2066
2077
|
innerKind === PACKET_ID_FILE_DATA ||
|
|
2067
2078
|
innerKind === PACKET_ID_FILE_FEC;
|
|
2068
2079
|
// Confirm the transport path + liveness for EVERY decrypted packet —
|
|
@@ -3807,7 +3818,15 @@ export class Peer {
|
|
|
3807
3818
|
// bulk-IP channel and lossy packets; the crypto nonce still advances above so
|
|
3808
3819
|
// decryption stays in sync regardless.
|
|
3809
3820
|
const isFileTransfer = kind === PACKET_ID_FILE_SENDREQUEST || kind === PACKET_ID_FILE_CONTROL || kind === PACKET_ID_FILE_DATA || kind === PACKET_ID_FILE_FEC;
|
|
3810
|
-
|
|
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;
|
|
3811
3830
|
const isDroppable = (isBulkData && !isFileTransfer) || kind >= 192;
|
|
3812
3831
|
if (!isDroppable) {
|
|
3813
3832
|
session.sendPacketNumber = (packetNumber + 1) >>> 0;
|
|
@@ -3848,20 +3867,12 @@ export class Peer {
|
|
|
3848
3867
|
// e.g. decentlan IP=163) stays bulk / single-transport to avoid the relay
|
|
3849
3868
|
// backlog + duplicate fan-out that high packet rates cause. Chat is sparse
|
|
3850
3869
|
// and net_crypto dedups by packet number, so dual-send is safe here.
|
|
3851
|
-
// File
|
|
3852
|
-
//
|
|
3853
|
-
//
|
|
3854
|
-
//
|
|
3855
|
-
//
|
|
3856
|
-
//
|
|
3857
|
-
// wrong order and never completes, while a tiny one (png, 1-2 chunks)
|
|
3858
|
-
// happens to finish before reordering bites. Pinning file transfer to a
|
|
3859
|
-
// single transport keeps the chunks in send order. (Reliability across a
|
|
3860
|
-
// lossy path still wants a reorder buffer + retransmit — tracked separately.)
|
|
3861
|
-
// File transfer rides the relay RELIABLY (never dropped under backpressure):
|
|
3862
|
-
// a dropped IP/CCTV packet is fine (the stream moves on), but a dropped file
|
|
3863
|
-
// chunk corrupts the file (no retransmit). So bulk IP data stays droppable
|
|
3864
|
-
// 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.)
|
|
3865
3876
|
await this.#sendToFriend(friendId, encrypted, session, isBulkData, isFileTransfer);
|
|
3866
3877
|
}
|
|
3867
3878
|
/**
|
|
@@ -3885,6 +3896,15 @@ export class Peer {
|
|
|
3885
3896
|
const udpFresh = !!realUdpRemote &&
|
|
3886
3897
|
s?.lastUdpRecvMs !== undefined &&
|
|
3887
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);
|
|
3888
3908
|
// Bulk IP data rides the direct path ONLY while it's fresh. When it
|
|
3889
3909
|
// goes stale (likely a NAT remap mid-stream), we do NOT keep firing
|
|
3890
3910
|
// into the dead endpoint and we do NOT duplicate onto UDP — we bridge
|
|
@@ -3892,7 +3912,8 @@ export class Peer {
|
|
|
3892
3912
|
// lastUdpRecvMs). This converts a multi-second blackout into a few
|
|
3893
3913
|
// seconds of higher relay latency. Control packets always probe UDP
|
|
3894
3914
|
// (to keep the NAT mapping warm) so we still try UDP for them.
|
|
3895
|
-
const tryUdp = realUdpRemote &&
|
|
3915
|
+
const tryUdp = realUdpRemote &&
|
|
3916
|
+
(isBulkData ? udpFresh && fileUdpOnlyConfirmed : true);
|
|
3896
3917
|
if (tryUdp && s?.remote) {
|
|
3897
3918
|
try {
|
|
3898
3919
|
await this.#sendPacket(packet, s.remote);
|
|
@@ -3902,9 +3923,9 @@ export class Peer {
|
|
|
3902
3923
|
firstError = error;
|
|
3903
3924
|
}
|
|
3904
3925
|
}
|
|
3905
|
-
// Fresh direct path → send over UDP only
|
|
3906
|
-
//
|
|
3907
|
-
//
|
|
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.
|
|
3908
3929
|
//
|
|
3909
3930
|
// Low-volume traffic (chat 64 + control keepalives / endpoint offers /
|
|
3910
3931
|
// profile) instead ALSO goes over the relay even when the direct path
|
|
@@ -3992,6 +4013,10 @@ export class Peer {
|
|
|
3992
4013
|
if (info.description !== undefined)
|
|
3993
4014
|
opts.statusMessage = info.description;
|
|
3994
4015
|
this.#profileSentTo.clear();
|
|
4016
|
+
for (const timer of this.#profileRetryTimers.values())
|
|
4017
|
+
clearTimeout(timer);
|
|
4018
|
+
this.#profileRetryTimers.clear();
|
|
4019
|
+
this.#profileRetryAttempts.clear();
|
|
3995
4020
|
for (const [friendId, s] of this.#friendSessions.entries()) {
|
|
3996
4021
|
if (s.established)
|
|
3997
4022
|
this.#sendProfileAndGreeting(friendId);
|
|
@@ -4005,6 +4030,25 @@ export class Peer {
|
|
|
4005
4030
|
description: opts.statusMessage ?? PEER_STATUS_MESSAGE,
|
|
4006
4031
|
};
|
|
4007
4032
|
}
|
|
4033
|
+
#scheduleProfileRetry(friendId) {
|
|
4034
|
+
if (this.#profileRetryTimers.has(friendId))
|
|
4035
|
+
return;
|
|
4036
|
+
const session = this.#friendSessions.get(friendId);
|
|
4037
|
+
if (!this.#started || !session?.established)
|
|
4038
|
+
return;
|
|
4039
|
+
const attempt = (this.#profileRetryAttempts.get(friendId) ?? 0) + 1;
|
|
4040
|
+
this.#profileRetryAttempts.set(friendId, attempt);
|
|
4041
|
+
const delayMs = Math.min(30_000, 1_000 * (2 ** Math.min(attempt - 1, 5)));
|
|
4042
|
+
const timer = setTimeout(() => {
|
|
4043
|
+
this.#profileRetryTimers.delete(friendId);
|
|
4044
|
+
if (this.#started && this.#friendSessions.get(friendId)?.established) {
|
|
4045
|
+
this.#sendProfileAndGreeting(friendId);
|
|
4046
|
+
}
|
|
4047
|
+
}, delayMs);
|
|
4048
|
+
timer.unref?.();
|
|
4049
|
+
this.#profileRetryTimers.set(friendId, timer);
|
|
4050
|
+
this.#debugLog(`profile retry ${attempt} for ${friendId} scheduled in ${delayMs}ms`);
|
|
4051
|
+
}
|
|
4008
4052
|
#sendProfileAndGreeting(friendId) {
|
|
4009
4053
|
if (!this.#profileSentTo.has(friendId)) {
|
|
4010
4054
|
this.#profileSentTo.add(friendId);
|
|
@@ -4044,9 +4088,16 @@ export class Peer {
|
|
|
4044
4088
|
});
|
|
4045
4089
|
await this.#sendMessengerPacket(friendId, PACKET_ID_STATUSMESSAGE, userInfo);
|
|
4046
4090
|
this.#debugLog(`userinfo sent to ${friendId} (name="${nick}", descr="${descr}", proto=${AGENTNET_PROTO_VERSION}, platform=${this.#opts.platform ?? process.platform})`);
|
|
4091
|
+
this.#profileRetryAttempts.delete(friendId);
|
|
4047
4092
|
}
|
|
4048
4093
|
catch (error) {
|
|
4094
|
+
// PACKET_ID_ONLINE can arrive a fraction before every transport is
|
|
4095
|
+
// ready to accept messenger packets. Do not leave the optimistic
|
|
4096
|
+
// "sent" marker wedged after that race: re-arm it and retry with
|
|
4097
|
+
// bounded backoff while this same session remains established.
|
|
4098
|
+
this.#profileSentTo.delete(friendId);
|
|
4049
4099
|
this.#debugLog(`send profile failed for ${friendId}: ${error.message}`);
|
|
4100
|
+
this.#scheduleProfileRetry(friendId);
|
|
4050
4101
|
}
|
|
4051
4102
|
})();
|
|
4052
4103
|
}
|
|
@@ -5378,6 +5429,11 @@ export class Peer {
|
|
|
5378
5429
|
// transitions prevents the user from seeing the same greeting message
|
|
5379
5430
|
// repeated each time the session bounces.
|
|
5380
5431
|
this.#profileSentTo.delete(friendId);
|
|
5432
|
+
const profileRetry = this.#profileRetryTimers.get(friendId);
|
|
5433
|
+
if (profileRetry)
|
|
5434
|
+
clearTimeout(profileRetry);
|
|
5435
|
+
this.#profileRetryTimers.delete(friendId);
|
|
5436
|
+
this.#profileRetryAttempts.delete(friendId);
|
|
5381
5437
|
const friend = this.#friends.get(friendId);
|
|
5382
5438
|
if (!friend) {
|
|
5383
5439
|
return;
|
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",
|