@decentnetwork/peer 0.1.52 → 0.1.53

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.
@@ -30,8 +30,16 @@ export const MAX_CONCURRENT_FILE_PIPES = 256;
30
30
  export const MAX_FILE_DATA_SIZE = 1367;
31
31
  export const FILECONTROL = { ACCEPT: 0, PAUSE: 1, KILL: 2, SEEK: 3, ACK: 4 };
32
32
  export const FILEKIND = { DATA: 0, AVATAR: 1 };
33
- // Reliability tuning.
34
- const WINDOW_CHUNKS = 192; // chunks in flight (≈ 256 KB) for throughput on high-BDP paths
33
+ // Reliability + congestion control tuning. The window is a CONGESTION WINDOW
34
+ // (cwnd, in chunks) that slow-starts from small and grows with acks, halving on
35
+ // a loss/stall — TCP-style AIMD. Starting at a fixed large window blasted an
36
+ // initial burst that a slow/lossy long-haul path (China↔US) couldn't absorb, so
37
+ // the front chunks kept dropping and go-back-N re-burst the same → a big file
38
+ // stuck at 0%. Slow-start makes the first send tiny (fits any path), then it
39
+ // self-clocks to the path's capacity via the ack stream.
40
+ const INITIAL_CWND = 8; // chunks (~11 KB) — small enough for any path
41
+ const MIN_CWND = 4;
42
+ const MAX_CWND = 1024; // ~1.4 MB in flight cap
35
43
  const RETRANSMIT_MS = 600; // resend window from the ack point if no progress in this long
36
44
  const ACK_THROTTLE_MS = 25; // coalesce receiver acks
37
45
  const SEND_GIVEUP_MS = 60000; // abandon a send after this long with no ack progress (peer gone)
@@ -129,7 +137,8 @@ export class FileTransferManager {
129
137
  const fileId = randomBytes(FILE_ID_LENGTH);
130
138
  const st = {
131
139
  fileNumber, fileId, name: opts.name, data, size: data.length,
132
- acked: 0, pumping: false, lastProgressAcked: 0, lastAckAdvanceMs: nowMs(),
140
+ acked: 0, cwnd: INITIAL_CWND, ssthresh: MAX_CWND,
141
+ pumping: false, lastProgressAcked: 0, lastAckAdvanceMs: nowMs(),
133
142
  };
134
143
  this.#map(this.#sending, friendId).set(fileNumber, st);
135
144
  const req = encodeFileSendRequest(fileNumber, opts.kind ?? FILEKIND.DATA, BigInt(data.length), fileId, opts.name);
@@ -194,7 +203,7 @@ export class FileTransferManager {
194
203
  const st = {
195
204
  fileNumber: r.fileNumber, fileId: r.fileId, name: r.filename, size,
196
205
  buf: new Uint8Array(size), got: new Uint8Array(chunkCount(size)),
197
- contiguous: 0, done: false, ackDirty: false,
206
+ contiguous: 0, maxByte: 0, done: false, ackDirty: false,
198
207
  };
199
208
  this.#map(this.#receiving, friendId).set(r.fileNumber, st);
200
209
  this.emit("file-offer", { friendId, fileNumber: r.fileNumber, fileId: hex(r.fileId), name: r.filename, size, kind: r.fileType });
@@ -249,6 +258,8 @@ export class FileTransferManager {
249
258
  const idx = Math.floor(offset / MAX_FILE_DATA_SIZE);
250
259
  if (offset % MAX_FILE_DATA_SIZE !== 0 || idx >= st.got.length)
251
260
  return; // not a chunk boundary
261
+ if (offset + data.length > st.maxByte)
262
+ st.maxByte = offset + data.length;
252
263
  if (!st.got[idx]) {
253
264
  st.buf.set(data, offset);
254
265
  st.got[idx] = 1;
@@ -304,17 +315,36 @@ export class FileTransferManager {
304
315
  #sendAck(friendId, st) {
305
316
  st.ackDirty = false;
306
317
  // send_receive=1: the ACK refers to a file the PEER is SENDING (it lives in
307
- // the peer's #sending table), so its #onControl routes it there.
308
- void this.send(friendId, PACKET_ID_FILE_CONTROL, encodeFileControl(1, st.fileNumber, FILECONTROL.ACK, u32be(st.contiguous))).catch(() => undefined);
318
+ // the peer's #sending table). Carries [contiguous][maxByte] so the sender
319
+ // sees the gap (maxByte contiguous) = loss/reorder, for congestion control.
320
+ const body = new Uint8Array(8);
321
+ body.set(u32be(st.contiguous), 0);
322
+ body.set(u32be(st.maxByte), 4);
323
+ void this.send(friendId, PACKET_ID_FILE_CONTROL, encodeFileControl(1, st.fileNumber, FILECONTROL.ACK, body)).catch(() => undefined);
309
324
  }
310
325
  // ── sender ────────────────────────────────────────────────────────
311
326
  #onAck(friendId, st, data) {
312
327
  if (data.length < 4)
313
328
  return;
314
329
  const ackedOffset = readU32be(data, 0);
330
+ const maxRecv = data.length >= 8 ? readU32be(data, 4) : ackedOffset;
331
+ // Gap = bytes the receiver has seen BEYOND the contiguous point that are
332
+ // still missing below maxRecv → loss (or reorder). A gap bigger than half
333
+ // the window means we're overrunning the path → back off (multiplicative
334
+ // decrease). Otherwise the path is keeping up → grow (slow-start / linear).
335
+ const gapChunks = Math.max(0, maxRecv - ackedOffset) / MAX_FILE_DATA_SIZE;
315
336
  if (ackedOffset > st.acked) {
316
337
  st.acked = Math.min(ackedOffset, st.size);
317
338
  st.lastAckAdvanceMs = nowMs();
339
+ if (gapChunks > st.cwnd * 0.5) {
340
+ st.ssthresh = Math.max(MIN_CWND, st.cwnd >> 1);
341
+ st.cwnd = st.ssthresh;
342
+ }
343
+ else {
344
+ st.cwnd = st.cwnd < st.ssthresh
345
+ ? Math.min(st.cwnd * 2, st.ssthresh)
346
+ : Math.min(st.cwnd + 1, MAX_CWND);
347
+ }
318
348
  if (st.acked - st.lastProgressAcked >= 64 * 1024 || st.acked >= st.size) {
319
349
  st.lastProgressAcked = st.acked;
320
350
  this.emit("file-progress", { friendId, fileId: hex(st.fileId), received: st.acked, total: st.size, sending: true });
@@ -357,6 +387,10 @@ export class FileTransferManager {
357
387
  this.emit("file-cancel", { friendId, fileId: hex(st.fileId), sending: true, reason: "timeout" });
358
388
  return;
359
389
  }
390
+ // Loss/stall detected → multiplicative decrease (back off), then resend
391
+ // from the ack point. This is the AIMD that paces us to the path.
392
+ st.ssthresh = Math.max(MIN_CWND, st.cwnd >> 1);
393
+ st.cwnd = st.ssthresh;
360
394
  st.lastAckAdvanceMs = nowMs();
361
395
  void this.#pumpFrom(friendId, st, st.acked);
362
396
  }, Math.ceil(RETRANSMIT_MS / 2));
@@ -370,9 +404,9 @@ export class FileTransferManager {
370
404
  st.pumping = false;
371
405
  }
372
406
  }
373
- // Send chunks from `from` up to the window edge (acked + window).
407
+ // Send chunks from `from` up to the congestion-window edge (acked + cwnd).
374
408
  async #pumpFrom(friendId, st, from) {
375
- const windowEnd = Math.min(st.acked + WINDOW_CHUNKS * MAX_FILE_DATA_SIZE, st.size);
409
+ const windowEnd = Math.min(st.acked + Math.max(MIN_CWND, st.cwnd | 0) * MAX_FILE_DATA_SIZE, st.size);
376
410
  let off = from;
377
411
  while (off < windowEnd) {
378
412
  if (!this.#map(this.#sending, friendId).has(st.fileNumber))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/peer",
3
- "version": "0.1.52",
3
+ "version": "0.1.53",
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",