@decentnetwork/peer 0.1.89 → 0.1.90

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.
Files changed (2) hide show
  1. package/dist/peer.js +58 -22
  2. package/package.json +2 -1
package/dist/peer.js CHANGED
@@ -174,7 +174,7 @@ const AGENTNET_PROTO_VERSION = 1;
174
174
  // Peer package version, advertised as the default appVersion when the embedder
175
175
  // doesn't override it. Read lazily so a bundler that inlines this file doesn't
176
176
  // need the package.json at runtime.
177
- const PEER_PKG_VERSION = "0.1.89";
177
+ const PEER_PKG_VERSION = "0.1.90";
178
178
  // Toxcore Messenger.h packet IDs (live inside encrypted 0x1b crypto data plain payload)
179
179
  const PACKET_ID_PADDING = 0;
180
180
  const PACKET_ID_REQUEST = 1; // request retransmission of unreceived packets
@@ -2032,8 +2032,25 @@ export class Peer {
2032
2032
  // De-duplicate: skip the DISPATCH + nonce advance for an already-seen
2033
2033
  // packet number. The transport confirmation above already ran, so a
2034
2034
  // duplicate still keeps the path fresh.
2035
+ //
2036
+ // EXCEPTION: a BULKMSG fragment (inline file from a native peer) can
2037
+ // arrive REORDERED — the C SDK puts totalsz on the FIRST fragment, but
2038
+ // over a dual/UDP path a later fragment can land first, advancing the
2039
+ // low-water past the first fragment's number. Dropping the first
2040
+ // fragment here makes every fragment read totalsz 0 → the whole file is
2041
+ // lost (the classic "bulkmsg totalsz 0 — dropped"). Let bulkmsg through;
2042
+ // #assembleBulkMsg dedups AND orders fragments by packet number, so a
2043
+ // reordered/duplicate fragment is handled correctly there instead.
2035
2044
  if (!isSingleTransportKind && opened.packetNumber < (state.receiveBufferStart ?? 0)) {
2036
- continue;
2045
+ let isBulk = false;
2046
+ if (innerKind === PACKET_ID_MESSAGE) {
2047
+ try {
2048
+ isBulk = decodeCarrierPacket(opened.payload.slice(1)).type === PACKET_TYPE_BULKMSG;
2049
+ }
2050
+ catch { /* not a carrier packet */ }
2051
+ }
2052
+ if (!isBulk)
2053
+ continue;
2037
2054
  }
2038
2055
  // Track the receive nonce from the EXACT nonce this packet used
2039
2056
  // (openCryptoDataPacket reconstructs it with full carry/borrow).
@@ -2078,7 +2095,7 @@ export class Peer {
2078
2095
  if (consumesReliableNumber) {
2079
2096
  state.receiveBufferStart = Math.max(state.receiveBufferStart ?? 0, (opened.packetNumber + 1) >>> 0);
2080
2097
  }
2081
- this.#deliverLosslessPayload(friendId, state, kind, inner, remote);
2098
+ this.#deliverLosslessPayload(friendId, state, kind, inner, remote, opened.packetNumber);
2082
2099
  return;
2083
2100
  }
2084
2101
  // Reverted: we used to DELETE a "desynced" session here to force a clean
@@ -3174,7 +3191,7 @@ export class Peer {
3174
3191
  * out of the datagram handler so the receive reorder buffer can replay
3175
3192
  * buffered packets through the exact same dispatch. `kind` is the first
3176
3193
  * payload byte (never undefined here — the caller defaults it). */
3177
- #deliverLosslessPayload(friendId, state, kind, inner, remote) {
3194
+ #deliverLosslessPayload(friendId, state, kind, inner, remote, packetNumber = 0) {
3178
3195
  // Toxcore file transfer (FILE_SENDREQUEST/CONTROL/DATA = 80/81/82).
3179
3196
  if (this.#fileTransfer.handlePacket(friendId, kind, inner))
3180
3197
  return;
@@ -3357,7 +3374,7 @@ export class Peer {
3357
3374
  return;
3358
3375
  }
3359
3376
  if (carrier?.type === PACKET_TYPE_BULKMSG) {
3360
- const complete = this.#assembleBulkMsg(friendId, carrier);
3377
+ const complete = this.#assembleBulkMsg(friendId, carrier, packetNumber);
3361
3378
  if (!complete)
3362
3379
  return; // more fragments pending
3363
3380
  text = decodeUtf8Best(complete);
@@ -3487,11 +3504,19 @@ export class Peer {
3487
3504
  incrementNonce(state.ourBaseNonce);
3488
3505
  await this.#sendToFriend(friendId, encrypted, state, false, false);
3489
3506
  }
3490
- /** Append a Carrier BULKMSG fragment; returns the full reassembled
3491
- * message when the last fragment lands, undefined while pending.
3492
- * Mirrors the C SDK's handle_friend_bulkmsg: totalsz on the first
3493
- * fragment sizes the buffer; fragments append in arrival order. */
3494
- #assembleBulkMsg(friendId, frag) {
3507
+ /** Append a Carrier BULKMSG fragment and return the full reassembled message
3508
+ * once every fragment has landed (undefined while pending).
3509
+ *
3510
+ * The C SDK's send_bulk_message puts totalsz on the FIRST fragment (index 1)
3511
+ * and sends the rest with totalsz 0, back-to-back with ascending reliable
3512
+ * packet numbers. Over a dual/UDP path those fragments can arrive REORDERED —
3513
+ * including the first (totalsz-bearing) one landing after later fragments. So
3514
+ * we key fragments by their PACKET NUMBER (dedup + order), record totalsz
3515
+ * whenever the first fragment shows up (in any order), and complete once we
3516
+ * hold every byte — then concatenate in packet-number order. This tolerates
3517
+ * reordering + duplicates WITHOUT stalling the reliable stream (a genuinely
3518
+ * lost fragment just expires this one message after 60s, nothing else). */
3519
+ #assembleBulkMsg(friendId, frag, packetNumber) {
3495
3520
  const now = Date.now();
3496
3521
  // Lazy expiry sweep (C uses a 60s assembly timeout).
3497
3522
  for (const [key, entry] of this.#bulkAssembly) {
@@ -3501,24 +3526,35 @@ export class Peer {
3501
3526
  const key = `${friendId}:${frag.tid.toString()}`;
3502
3527
  let entry = this.#bulkAssembly.get(key);
3503
3528
  if (!entry) {
3504
- if (!frag.totalsz || frag.totalsz > CARRIER_MAX_APP_BULKMSG_LEN) {
3505
- this.#debugLog(`bulkmsg from ${friendId} with invalid/missing totalsz ${frag.totalsz} — dropped`);
3529
+ entry = { total: 0, frags: new Map(), got: 0, expireAtMs: now + 60_000 };
3530
+ this.#bulkAssembly.set(key, entry);
3531
+ }
3532
+ // The first fragment carries totalsz; it may arrive out of order.
3533
+ if (frag.totalsz > 0) {
3534
+ if (frag.totalsz > CARRIER_MAX_APP_BULKMSG_LEN) {
3535
+ this.#debugLog(`bulkmsg from ${friendId} totalsz ${frag.totalsz} exceeds cap — dropped`);
3536
+ this.#bulkAssembly.delete(key);
3506
3537
  return undefined;
3507
3538
  }
3508
- entry = { total: frag.totalsz, chunks: [], got: 0, expireAtMs: now + 60_000 };
3509
- this.#bulkAssembly.set(key, entry);
3539
+ entry.total = frag.totalsz;
3510
3540
  }
3511
- if (frag.data.length === 0 || entry.got + frag.data.length > entry.total) {
3512
- this.#debugLog(`bulkmsg fragment from ${friendId} overflows totalsz — dropped`);
3513
- this.#bulkAssembly.delete(key);
3514
- return undefined;
3541
+ // Store by packet number; a duplicate (dual-send) fragment is ignored.
3542
+ if (frag.data.length > 0 && !entry.frags.has(packetNumber)) {
3543
+ entry.frags.set(packetNumber, frag.data);
3544
+ entry.got += frag.data.length;
3515
3545
  }
3516
- entry.chunks.push(frag.data);
3517
- entry.got += frag.data.length;
3518
- if (entry.got < entry.total)
3546
+ // Complete only once the totalsz-bearing fragment has arrived AND we hold
3547
+ // every byte. Until then, keep buffering.
3548
+ if (entry.total === 0 || entry.got < entry.total)
3519
3549
  return undefined;
3520
3550
  this.#bulkAssembly.delete(key);
3521
- return concatBytes(entry.chunks);
3551
+ if (entry.got > entry.total) {
3552
+ this.#debugLog(`bulkmsg from ${friendId} got ${entry.got} > total ${entry.total} — dropped`);
3553
+ return undefined;
3554
+ }
3555
+ // Concatenate in ascending packet-number order (the fragments' send order).
3556
+ const nums = [...entry.frags.keys()].sort((a, b) => a - b);
3557
+ return concatBytes(nums.map((n) => entry.frags.get(n)));
3522
3558
  }
3523
3559
  /** Append a Carrier invite fragment (INVITE_REQUEST/RESPONSE); returns the
3524
3560
  * full reassembled payload when the last fragment lands, undefined while
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/peer",
3
- "version": "0.1.89",
3
+ "version": "0.1.90",
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",
@@ -69,6 +69,7 @@
69
69
  "test:compat": "pnpm run build && node scripts/compat-selftest.mjs",
70
70
  "test:invite": "pnpm run build && node scripts/invite-selftest.mjs",
71
71
  "test:reorder": "pnpm run build && node scripts/reorder-selftest.mjs",
72
+ "test:bulkmsg": "pnpm run build && node scripts/bulkmsg-selftest.mjs",
72
73
  "test:userinfo": "pnpm run build && node scripts/userinfo-selftest.mjs",
73
74
  "test:tcp-onion": "pnpm run build && node scripts/tcp-onion-selftest.mjs",
74
75
  "smoke:join": "node scripts/smoke-join.mjs",