@decentnetwork/peer 0.1.105 → 0.1.108
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 +6 -0
- package/dist/compat/filetransfer.js +66 -5
- package/dist/peer.d.ts +5 -0
- package/dist/peer.js +55 -1
- package/package.json +1 -1
|
@@ -57,6 +57,12 @@ export declare class FileTransferManager {
|
|
|
57
57
|
accept(friendId: string, fileNumber: number): void;
|
|
58
58
|
cancel(friendId: string, fileNumber: number, isSending: boolean): void;
|
|
59
59
|
cancelByFileId(friendId: string, fileIdHex: string, isSending: boolean): boolean;
|
|
60
|
+
/** Cancel in-flight sends matching both name and size.
|
|
61
|
+
* This is a conservative registry-desync fallback after fileId/hash lookup. */
|
|
62
|
+
cancelSendsMatching(friendId: string, match: {
|
|
63
|
+
name?: string;
|
|
64
|
+
size: number;
|
|
65
|
+
}): string[];
|
|
60
66
|
handlePacket(friendId: string, packetId: number, payload: Uint8Array): boolean;
|
|
61
67
|
clearFriend(friendId: string): void;
|
|
62
68
|
}
|
|
@@ -119,6 +119,8 @@ const LAN_STARTUP_QUEUE_MS = 500; // unmistakable raw queue spike; smaller app-l
|
|
|
119
119
|
const LAN_STARTUP_HARD_QUEUE_MS = 2000; // one multi-second queue sample is unsafe
|
|
120
120
|
const LAN_STARTUP_QUEUE_ROUNDS = 2; // tolerate one scheduler/session-handover outlier on LAN
|
|
121
121
|
const LAN_STARTUP_KEEPUP = 0.70; // delivered goodput below 70% of pace means the receiver is no longer keeping up
|
|
122
|
+
const LAN_RECOVERY_GAIN = 1.20; // after a LAN overshoot drains, climb out of the 120KB/s floor quickly but smoothly
|
|
123
|
+
const LAN_RECOVERY_QUEUE_MS = 250; // only recover while queueing is clearly gone
|
|
122
124
|
// Five 200 ms samples was still only about one Windows ACK scheduling cycle in
|
|
123
125
|
// practice: a clean LAN (0% loss) exited STARTUP after ~3 MB and threw away a
|
|
124
126
|
// healthy 2.3 MB/s probe. Require a few seconds of sustained under-delivery;
|
|
@@ -280,6 +282,8 @@ export class FileTransferManager {
|
|
|
280
282
|
btlBwBps: 0, bwSamples: [], fullBwRounds: 0, startupQueueRounds: 0, bbrPhase: 0, roundCount: 0,
|
|
281
283
|
rateMarkMs: nowMs(), rateMarkAcked: 0, lowRttCount: 0, lowRttMinMs: Infinity, lastCcLogMs: 0,
|
|
282
284
|
lanPath,
|
|
285
|
+
dbgLanNow: lanPath ? 1 : 0, dbgSampleMs: 0, dbgRoundRate: 0, dbgMeasuredRate: 0,
|
|
286
|
+
dbgRawQueue: 0, dbgUnder: 0, dbgOvershot: 0,
|
|
283
287
|
};
|
|
284
288
|
this.#map(this.#sending, friendId).set(fileNumber, st);
|
|
285
289
|
void this.send(friendId, PACKET_ID_FILE_SENDREQUEST, req).catch(() => undefined);
|
|
@@ -330,27 +334,54 @@ export class FileTransferManager {
|
|
|
330
334
|
}
|
|
331
335
|
}
|
|
332
336
|
cancel(friendId, fileNumber, isSending) {
|
|
337
|
+
const inner = (isSending ? this.#sending : this.#receiving).get(friendId);
|
|
338
|
+
const st = inner?.get(fileNumber);
|
|
339
|
+
const fileId = st ? hex(st.fileId) : undefined;
|
|
333
340
|
const sr = isSending ? 0 : 1;
|
|
334
341
|
void this.send(friendId, PACKET_ID_FILE_CONTROL, encodeFileControl(sr, fileNumber, FILECONTROL.KILL)).catch(() => undefined);
|
|
335
342
|
if (isSending)
|
|
336
343
|
this.#endSend(friendId, fileNumber);
|
|
337
344
|
else
|
|
338
345
|
this.#endRecv(friendId, fileNumber);
|
|
346
|
+
// Local cancel used to tear down silently. The daemon tracks sends in
|
|
347
|
+
// activeSends and needs this event when the registry entry is already gone
|
|
348
|
+
// (or cancel raced with a progress tick) so the UI chip cannot stay "sending".
|
|
349
|
+
if (fileId)
|
|
350
|
+
this.emit("file-cancel", { friendId, fileId, sending: isSending, reason: "local" });
|
|
339
351
|
}
|
|
340
352
|
// Cancel by content fileId (hex) — the daemon/UI tracks transfers by fileId,
|
|
341
353
|
// not the internal fileNumber, so it can stop a specific in-flight transfer.
|
|
342
354
|
cancelByFileId(friendId, fileIdHex, isSending) {
|
|
355
|
+
const want = fileIdHex.trim().toLowerCase();
|
|
343
356
|
const inner = (isSending ? this.#sending : this.#receiving).get(friendId);
|
|
344
357
|
if (!inner)
|
|
345
358
|
return false;
|
|
346
359
|
for (const [fileNumber, st] of inner) {
|
|
347
|
-
if (hex(st.fileId) ===
|
|
360
|
+
if (hex(st.fileId) === want) {
|
|
348
361
|
this.cancel(friendId, fileNumber, isSending);
|
|
349
362
|
return true;
|
|
350
363
|
}
|
|
351
364
|
}
|
|
352
365
|
return false;
|
|
353
366
|
}
|
|
367
|
+
/** Cancel in-flight sends matching both name and size.
|
|
368
|
+
* This is a conservative registry-desync fallback after fileId/hash lookup. */
|
|
369
|
+
cancelSendsMatching(friendId, match) {
|
|
370
|
+
const inner = this.#sending.get(friendId);
|
|
371
|
+
if (!inner)
|
|
372
|
+
return [];
|
|
373
|
+
const candidates = [...inner].filter(([, st]) => st.size === match.size);
|
|
374
|
+
const named = match.name
|
|
375
|
+
? candidates.filter(([, st]) => st.name === match.name)
|
|
376
|
+
: [];
|
|
377
|
+
const picked = named;
|
|
378
|
+
const killed = [];
|
|
379
|
+
for (const [fileNumber, st] of picked) {
|
|
380
|
+
killed.push(hex(st.fileId));
|
|
381
|
+
this.cancel(friendId, fileNumber, true);
|
|
382
|
+
}
|
|
383
|
+
return killed;
|
|
384
|
+
}
|
|
354
385
|
handlePacket(friendId, packetId, payload) {
|
|
355
386
|
if (packetId === PACKET_ID_FILE_SENDREQUEST) {
|
|
356
387
|
this.#onSendRequest(friendId, payload);
|
|
@@ -798,6 +829,12 @@ export class FileTransferManager {
|
|
|
798
829
|
const measuredRate = lanNow
|
|
799
830
|
? Math.min(roundRate, Math.max(PACE_INIT_BPS, st.paceRateBps * LAN_SAMPLE_MAX_GAIN))
|
|
800
831
|
: roundRate;
|
|
832
|
+
st.dbgLanNow = lanNow ? 1 : 0;
|
|
833
|
+
st.dbgSampleMs = Math.round(sampleMs);
|
|
834
|
+
st.dbgRoundRate = Math.round(roundRate);
|
|
835
|
+
st.dbgMeasuredRate = Math.round(measuredRate);
|
|
836
|
+
st.dbgUnder = 0;
|
|
837
|
+
st.dbgOvershot = 0;
|
|
801
838
|
if (measuredRate > 0) {
|
|
802
839
|
st.bwSamples.push(measuredRate);
|
|
803
840
|
if (st.bwSamples.length > PACE_BW_WINDOW)
|
|
@@ -822,6 +859,8 @@ export class FileTransferManager {
|
|
|
822
859
|
st.cwnd = Math.max(st.cwnd, LAN_CWND_INIT_CHUNKS);
|
|
823
860
|
}
|
|
824
861
|
st.lanPath = lanNow;
|
|
862
|
+
const rawQueue = st.minRttMs !== Infinity ? acceptedRtt - st.minRttMs : 0;
|
|
863
|
+
st.dbgRawQueue = Math.round(rawQueue);
|
|
825
864
|
if (!lanNow) {
|
|
826
865
|
// Relay/public-NAT file traffic must remain conservative. A short
|
|
827
866
|
// ACK burst can wildly overstate bandwidth there; allowing LAN's
|
|
@@ -849,7 +888,6 @@ export class FileTransferManager {
|
|
|
849
888
|
else {
|
|
850
889
|
st.fullBwRounds++;
|
|
851
890
|
}
|
|
852
|
-
const rawQueue = st.minRttMs !== Infinity ? acceptedRtt - st.minRttMs : 0;
|
|
853
891
|
if (rawQueue > LAN_STARTUP_QUEUE_MS) {
|
|
854
892
|
st.startupQueueRounds++;
|
|
855
893
|
}
|
|
@@ -864,6 +902,8 @@ export class FileTransferManager {
|
|
|
864
902
|
const startupOvershot = rawQueue > LAN_STARTUP_HARD_QUEUE_MS ||
|
|
865
903
|
st.startupQueueRounds >= LAN_STARTUP_QUEUE_ROUNDS;
|
|
866
904
|
const startupUnderDelivering = st.fullBwRounds >= LAN_STARTUP_SLOW_ROUNDS;
|
|
905
|
+
st.dbgUnder = startupUnderDelivering ? 1 : 0;
|
|
906
|
+
st.dbgOvershot = startupOvershot ? 1 : 0;
|
|
867
907
|
if (startupUnderDelivering || startupOvershot) {
|
|
868
908
|
st.bbrPhase = 2;
|
|
869
909
|
}
|
|
@@ -885,8 +925,18 @@ export class FileTransferManager {
|
|
|
885
925
|
}
|
|
886
926
|
else {
|
|
887
927
|
// PROBE_BW cruise: one sample probes up, one drains, six cruise.
|
|
928
|
+
// If LAN STARTUP hit one real overshoot (e.g. a 1s ACK pause with
|
|
929
|
+
// high reported loss), the normal max-filter can be rebuilt from
|
|
930
|
+
// tiny post-drain samples and then takes tens of seconds to climb
|
|
931
|
+
// out of the 120KB/s floor. Once loss and queueing are gone, let
|
|
932
|
+
// a validated LAN recover toward its safe initial probe rate. The
|
|
933
|
+
// existing loss/queue brakes still stop a bad path immediately.
|
|
888
934
|
const gain = PACE_CYCLE_GAINS[st.roundCount % PACE_CYCLE_GAINS.length];
|
|
889
|
-
|
|
935
|
+
const cruiseRate = Math.max(PACE_INIT_BPS * 0.5, st.btlBwBps * gain * lossMult);
|
|
936
|
+
const healthyLan = lanNow && st.lossEwma < 0.02 && rawQueue < LAN_RECOVERY_QUEUE_MS;
|
|
937
|
+
st.paceRateBps = healthyLan
|
|
938
|
+
? Math.max(cruiseRate, Math.min(LAN_PACE_INIT_BPS, Math.max(st.paceRateBps * LAN_RECOVERY_GAIN, measuredRate * LAN_STARTUP_DELIVERY_GAIN)))
|
|
939
|
+
: cruiseRate;
|
|
890
940
|
}
|
|
891
941
|
// Hard safety cap: never pace faster than a full window drains in 1 RTT.
|
|
892
942
|
if (st.minRttMs !== Infinity && st.minRttMs > 0) {
|
|
@@ -904,15 +954,26 @@ export class FileTransferManager {
|
|
|
904
954
|
}
|
|
905
955
|
if (process.env.DECENT_DEBUG && nowT - st.lastCcLogMs >= 1000) {
|
|
906
956
|
st.lastCcLogMs = nowT;
|
|
957
|
+
const windowEnd = Math.min(st.acked + st.cwnd * MAX_FILE_DATA_SIZE, st.size);
|
|
907
958
|
console.error(`[file-cc] friend=${friendId.slice(0, 8)} ack=${st.acked}/${st.size}` +
|
|
908
959
|
` rtt=${rtt}ms srtt=${st.srttMs.toFixed(1)}ms low=${st.lowRttCount}` +
|
|
909
960
|
` accepted=${(!lowSample || promoteLowPath) ? 1 : 0}` +
|
|
910
961
|
` pace=${Math.round(st.paceRateBps)}Bps bw=${Math.round(st.btlBwBps)}Bps` +
|
|
911
962
|
` cwnd=${st.cwnd} phase=${st.bbrPhase} slow=${st.fullBwRounds}` +
|
|
912
|
-
` loss=${(st.lossEwma * 100).toFixed(1)}% rounds=${st.roundCount}`
|
|
963
|
+
` loss=${(st.lossEwma * 100).toFixed(1)}% rounds=${st.roundCount}` +
|
|
964
|
+
` lanNow=${st.dbgLanNow} lanPath=${st.lanPath ? 1 : 0}` +
|
|
965
|
+
` sampleMs=${st.dbgSampleMs} roundRate=${st.dbgRoundRate}` +
|
|
966
|
+
` measuredRate=${st.dbgMeasuredRate} rawQueue=${st.dbgRawQueue}` +
|
|
967
|
+
` startupQueueRounds=${st.startupQueueRounds}` +
|
|
968
|
+
` under=${st.dbgUnder} overshot=${st.dbgOvershot}` +
|
|
969
|
+
` rateMarkAcked=${st.rateMarkAcked} nextSend=${st.nextSend}` +
|
|
970
|
+
` windowEnd=${windowEnd} tokens=${Math.round(st.paceTokens)}`);
|
|
913
971
|
}
|
|
914
972
|
}
|
|
915
|
-
|
|
973
|
+
// Small files (UI screenshots etc.) used to sit at 0% until the final
|
|
974
|
+
// byte because progress only fired every 256 KB. Emit at least every
|
|
975
|
+
// 16 KB or on completion so a 100–200 KB send shows movement.
|
|
976
|
+
if (st.acked - st.lastProgressAcked >= 16 * 1024 || st.acked >= st.size) {
|
|
916
977
|
st.lastProgressAcked = st.acked;
|
|
917
978
|
this.emit("file-progress", { friendId, fileId: hex(st.fileId), received: st.acked, total: st.size, sending: true });
|
|
918
979
|
}
|
package/dist/peer.d.ts
CHANGED
|
@@ -164,6 +164,11 @@ export declare class Peer {
|
|
|
164
164
|
cancelFile(userid: string, fileNumber: number, isSending?: boolean): void;
|
|
165
165
|
/** Cancel an in-flight transfer by its content fileId (hex). Returns true if found. */
|
|
166
166
|
cancelFileById(userid: string, fileId: string, isSending?: boolean): boolean;
|
|
167
|
+
/** Cancel in-flight sends that match size/name (registry-desync fallback). */
|
|
168
|
+
cancelSendsMatching(userid: string, match: {
|
|
169
|
+
name?: string;
|
|
170
|
+
size: number;
|
|
171
|
+
}): string[];
|
|
167
172
|
/** Incoming file offer: { friendId, fileNumber, fileId, name, size, kind }. */
|
|
168
173
|
onFile(cb: (offer: {
|
|
169
174
|
friendId: string;
|
package/dist/peer.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
2
4
|
import { mkdir, readFile, writeFile, rename } from "node:fs/promises";
|
|
3
5
|
import { networkInterfaces } from "node:os";
|
|
4
6
|
import { dirname, join } from "node:path";
|
|
@@ -1348,6 +1350,10 @@ export class Peer {
|
|
|
1348
1350
|
cancelFile(userid, fileNumber, isSending = false) { this.#fileTransfer.cancel(userid, fileNumber, isSending); }
|
|
1349
1351
|
/** Cancel an in-flight transfer by its content fileId (hex). Returns true if found. */
|
|
1350
1352
|
cancelFileById(userid, fileId, isSending = false) { return this.#fileTransfer.cancelByFileId(userid, fileId, isSending); }
|
|
1353
|
+
/** Cancel in-flight sends that match size/name (registry-desync fallback). */
|
|
1354
|
+
cancelSendsMatching(userid, match) {
|
|
1355
|
+
return this.#fileTransfer.cancelSendsMatching(userid, match);
|
|
1356
|
+
}
|
|
1351
1357
|
/** Incoming file offer: { friendId, fileNumber, fileId, name, size, kind }. */
|
|
1352
1358
|
onFile(cb) { this.#events.on("file-offer", cb); }
|
|
1353
1359
|
/** Transfer progress: { friendId, fileId, received, total, sending? }. */
|
|
@@ -4489,7 +4495,11 @@ export class Peer {
|
|
|
4489
4495
|
// office silently fall back to the TURN relay (~260ms for a <1ms hop).
|
|
4490
4496
|
const lanIp = getPhysicalLanAddresses().find((ip) => isPrivateAddress(ip));
|
|
4491
4497
|
const lanOctets = lanIp ? lanIp.split(".").map((s) => parseInt(s, 10)) : undefined;
|
|
4492
|
-
|
|
4498
|
+
// WSL packets are NATed by Windows, so the physical host NIC must be paired
|
|
4499
|
+
// with the observed server-reflexive port, not WSL's internal socket port.
|
|
4500
|
+
const lanPort = lanIp && _wslHostAddrsCache.has(lanIp)
|
|
4501
|
+
? srflx.port
|
|
4502
|
+
: (this.#udp.localPort() ?? 0);
|
|
4493
4503
|
const lanValid = !!lanOctets && lanOctets.length === 4 && !lanOctets.some((o) => Number.isNaN(o)) && lanPort > 0;
|
|
4494
4504
|
const size = lanValid ? 18 : relayValid ? 12 : 6;
|
|
4495
4505
|
const payload = new Uint8Array(size);
|
|
@@ -6327,6 +6337,45 @@ let _lanAddrsCache = [];
|
|
|
6327
6337
|
let _lanSubnetsCache = [];
|
|
6328
6338
|
let _allOwnAddrsCache = new Set();
|
|
6329
6339
|
let _ownVirtualAddrsCache = new Set();
|
|
6340
|
+
let _wslHostAddrsCache = new Set();
|
|
6341
|
+
let _wslHostCacheMs = 0;
|
|
6342
|
+
/** Discover the Windows host's physical NIC from WSL2. WSL itself only sees
|
|
6343
|
+
* its 172.x Hyper-V NIC, which is unreachable from another LAN machine. */
|
|
6344
|
+
function getWslWindowsHostAddresses(localSubnets) {
|
|
6345
|
+
const now = Date.now();
|
|
6346
|
+
if (now - _wslHostCacheMs < 60_000)
|
|
6347
|
+
return [..._wslHostAddrsCache];
|
|
6348
|
+
_wslHostCacheMs = now;
|
|
6349
|
+
const found = new Set();
|
|
6350
|
+
const ipconfig = "/mnt/c/Windows/System32/ipconfig.exe";
|
|
6351
|
+
if (process.platform !== "linux" || !existsSync(ipconfig)) {
|
|
6352
|
+
_wslHostAddrsCache = found;
|
|
6353
|
+
return [];
|
|
6354
|
+
}
|
|
6355
|
+
try {
|
|
6356
|
+
const output = execFileSync(ipconfig, [], {
|
|
6357
|
+
encoding: "utf8",
|
|
6358
|
+
timeout: 2000,
|
|
6359
|
+
windowsHide: true,
|
|
6360
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
6361
|
+
});
|
|
6362
|
+
// "IPv4" remains in localized ipconfig output (including Chinese).
|
|
6363
|
+
for (const match of output.matchAll(/IPv4[^:\r\n]*:\s*(\d+\.\d+\.\d+\.\d+)/gi)) {
|
|
6364
|
+
const host = match[1];
|
|
6365
|
+
if (!isPrivateAddress(host) || isCgnatAddress(host) || host.startsWith("127.") || host.startsWith("169.254."))
|
|
6366
|
+
continue;
|
|
6367
|
+
// Exclude WSL/Hyper-V addresses already visible inside Linux.
|
|
6368
|
+
if (localSubnets.some((s) => isInIpv4Subnet(host, s)))
|
|
6369
|
+
continue;
|
|
6370
|
+
found.add(host);
|
|
6371
|
+
}
|
|
6372
|
+
}
|
|
6373
|
+
catch {
|
|
6374
|
+
// WSL interop can be disabled; ordinary interface discovery remains valid.
|
|
6375
|
+
}
|
|
6376
|
+
_wslHostAddrsCache = found;
|
|
6377
|
+
return [...found];
|
|
6378
|
+
}
|
|
6330
6379
|
function refreshLanIfaceCache() {
|
|
6331
6380
|
const now = Date.now();
|
|
6332
6381
|
if (_lanIfaceCacheMs !== 0 && now - _lanIfaceCacheMs < 5000)
|
|
@@ -6363,6 +6412,11 @@ function refreshLanIfaceCache() {
|
|
|
6363
6412
|
catch {
|
|
6364
6413
|
// best-effort
|
|
6365
6414
|
}
|
|
6415
|
+
// Prefer the physical Windows host NIC over WSL's internal eth0 address.
|
|
6416
|
+
const wslHostAddrs = getWslWindowsHostAddresses(subnets);
|
|
6417
|
+
for (const host of wslHostAddrs)
|
|
6418
|
+
allOwn.add(host);
|
|
6419
|
+
addrs.unshift(...wslHostAddrs);
|
|
6366
6420
|
_lanAddrsCache = addrs;
|
|
6367
6421
|
_lanSubnetsCache = subnets;
|
|
6368
6422
|
_allOwnAddrsCache = allOwn;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.108",
|
|
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",
|