@decentnetwork/peer 0.1.139 → 0.1.140

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/index.d.ts CHANGED
@@ -14,4 +14,4 @@ export { LegacyProtocolNotImplementedError } from "./runtime/errors.js";
14
14
  export type { CarrierPacket, FriendMessagePacket, FriendRequestPacket, InviteReqPacket, InviteRspPacket } from "./compat/packet.js";
15
15
  export type { ToxDhtCryptoRequest } from "./compat/tox-dht-crypto.js";
16
16
  export type { CarrierAddressParts } from "./compat/address.js";
17
- export type { CompatibilityMode, CustomPacketEvent, FriendConnectionEvent, FriendConnectionStatus, FriendInfoEvent, FriendRequest, InlineFileEvent, InviteEvent, InviteResponseEvent, LookupResult, NetworkNode, PeerOptions, SendTextUntilAckOptions, TextMessage } from "./types/peer.js";
17
+ export type { CompatibilityMode, CustomPacketEvent, FriendConnectionEvent, FriendConnectionStatus, FriendInfoEvent, FriendRequest, InlineFileEvent, InlineSendResult, InviteEvent, InviteResponseEvent, LookupResult, NetworkNode, PeerOptions, SendTextUntilAckOptions, TextMessage } from "./types/peer.js";
package/dist/peer.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type BootstrapResult } from "./compat/bootstrap.js";
2
2
  import type { FriendRecord } from "./store/friends.js";
3
3
  import { type CarrierTurnServerInfo, type RtcIceServer } from "./ice-servers.js";
4
- import type { CustomPacketEvent, FriendConnectionEvent, FriendRequest, FriendInfoEvent, InlineFileEvent, InviteEvent, InviteResponseEvent, LookupResult, NetworkNode, PeerOptions, SendTextUntilAckOptions, TextMessage } from "./types/peer.js";
4
+ import type { CustomPacketEvent, FriendConnectionEvent, FriendRequest, FriendInfoEvent, InlineFileEvent, InlineSendResult, InviteEvent, InviteResponseEvent, LookupResult, NetworkNode, PeerOptions, SendTextUntilAckOptions, TextMessage } from "./types/peer.js";
5
5
  export declare class Peer {
6
6
  #private;
7
7
  private constructor();
@@ -140,7 +140,16 @@ export declare class Peer {
140
140
  name: string;
141
141
  data: Uint8Array;
142
142
  fileType?: "image" | "audio" | "text" | "unknown";
143
- }): Promise<void>;
143
+ deliveryTimeoutMs?: number;
144
+ }): Promise<InlineSendResult>;
145
+ /**
146
+ * True when this friend is a NATIVE Carrier client (iOS/Android/C SDK): its
147
+ * DHT/relay key differs from its identity key, where a JS peer announces its
148
+ * identity key as its DHT key. Natives cannot see the toxcore file-transfer
149
+ * protocol (sendFile) — the inline envelope is their only file path — and
150
+ * they never speak the JS text-ACK scheme.
151
+ */
152
+ isNativeFriend(pubkey: string): boolean;
144
153
  /** Fired when a peer sends a Carrier friend-invite (PACKET_TYPE_INVITE_REQUEST).
145
154
  * This is the channel the iOS/Android WebRTC SDK uses for call signaling —
146
155
  * each RtcSignal arrives as an "invite" with ext="carrier" and `data` the
package/dist/peer.js CHANGED
@@ -1240,6 +1240,9 @@ export class Peer {
1240
1240
  }
1241
1241
  await this.#sendTextPlain(pubkey, text);
1242
1242
  }
1243
+ /** What #sendTextPlain actually did with the message — callers that promise
1244
+ * delivery semantics (inline files) need to know the path and, for the live
1245
+ * path, where the send window stood after the last fragment. */
1243
1246
  async #sendTextPlain(pubkey, text) {
1244
1247
  const friend = this.#friends.get(pubkey);
1245
1248
  if (!friend) {
@@ -1307,7 +1310,10 @@ export class Peer {
1307
1310
  for (const p of livePackets) {
1308
1311
  await this.#sendMessengerPacket(pubkey, PACKET_ID_MESSAGE, p);
1309
1312
  }
1310
- return;
1313
+ // Every fragment got a packet number BELOW the counter's position now —
1314
+ // when the peer's implicit acks advance the window past it, the whole
1315
+ // message is confirmed on their device.
1316
+ return { via: "online", endPacketNumber: this.#friendSessions.get(pubkey)?.sendPacketNumber };
1311
1317
  }
1312
1318
  catch (error) {
1313
1319
  const emsg = error.message;
@@ -1343,7 +1349,7 @@ export class Peer {
1343
1349
  // no session is simply a no-op.
1344
1350
  if (text.length === 0) {
1345
1351
  this.#debugLog(`sendText: empty connection-kick to ${pubkey} with no live session — dropping (not flooding express)`);
1346
- return;
1352
+ return { via: "online" };
1347
1353
  }
1348
1354
  // Offline / fallback path via Carrier express HTTP store-and-forward.
1349
1355
  // Skipped entirely in control-plane-only mode (decentlan's data plane):
@@ -1353,7 +1359,7 @@ export class Peer {
1353
1359
  if (this.#express?.hasNodes() && !this.#opts.expressControlPlaneOnly) {
1354
1360
  await this.#express.sendOfflineText(pubkey, packet);
1355
1361
  this.#debugLog(`sendText: queued via express for ${pubkey}`);
1356
- return;
1362
+ return { via: "offline" };
1357
1363
  }
1358
1364
  throw new Error("friend is offline and no express node is configured");
1359
1365
  }
@@ -1568,7 +1574,77 @@ export class Peer {
1568
1574
  if (envelope.length > CARRIER_MAX_APP_BULKMSG_LEN) {
1569
1575
  throw new Error(`inline file too large for the message channel (${envelope.length} bytes encoded, max ${CARRIER_MAX_APP_BULKMSG_LEN})`);
1570
1576
  }
1571
- await this.sendText(pubkey, envelope);
1577
+ // The confirmation window scales with the payload: a floor for handshake
1578
+ // latency plus time to move the envelope at a conservative 50 KB/s, capped
1579
+ // so a dead session can't pin the caller for ages.
1580
+ const deliveryTimeoutMs = opts.deliveryTimeoutMs ??
1581
+ Math.min(180_000, 20_000 + Math.ceil(envelope.length / 50));
1582
+ if (this.#shouldRequireTextAck(pubkey)) {
1583
+ // JS peer: the app-level text-ACK is the strongest confirmation there is
1584
+ // — the receiving app parsed the envelope. The retry interval must be
1585
+ // longer than one full send of the payload, or a slow path gets the
1586
+ // whole multi-MB envelope re-blasted mid-flight.
1587
+ await this.sendTextUntilAck(pubkey, envelope, {
1588
+ timeoutMs: deliveryTimeoutMs,
1589
+ retryIntervalMs: Math.max(TEXT_ACK_RETRY_MS, Math.ceil(envelope.length / 100))
1590
+ });
1591
+ return { delivery: "acked" };
1592
+ }
1593
+ // Native peer (iOS/Android/C): it will never speak our text-ACK envelope,
1594
+ // but its toxcore DOES acknowledge every reliable packet — the very signal
1595
+ // its own SDK surfaces to the app as "Delivered". Wait for the send window
1596
+ // to drain past our fragments.
1597
+ const outcome = await this.#sendTextPlain(pubkey, envelope);
1598
+ if (outcome.via === "offline")
1599
+ return { delivery: "offline" };
1600
+ if (outcome.endPacketNumber === undefined)
1601
+ return { delivery: "accepted" };
1602
+ const acked = await this.#awaitTransportAck(pubkey, outcome.endPacketNumber, deliveryTimeoutMs);
1603
+ this.#debugLog(`inline file "${opts.name}" to ${pubkey}: transport ${acked ? "ACKED" : `NOT confirmed within ${deliveryTimeoutMs}ms`}`);
1604
+ return { delivery: acked ? "acked" : "accepted" };
1605
+ }
1606
+ /**
1607
+ * True once the friend's implicit acks (PACKET_ID_REQUEST walks) have moved
1608
+ * our reliable send window past `endPacketNumber` — i.e. every packet we had
1609
+ * sent by that point is confirmed received on their device. False when the
1610
+ * window doesn't drain in time or the session dies/rekeys under us (a new
1611
+ * session restarts the numbering, so the old position proves nothing).
1612
+ */
1613
+ async #awaitTransportAck(pubkey, endPacketNumber, timeoutMs) {
1614
+ const watched = this.#friendSessions.get(pubkey);
1615
+ if (!watched)
1616
+ return false;
1617
+ const deadline = Date.now() + timeoutMs;
1618
+ for (;;) {
1619
+ const session = this.#friendSessions.get(pubkey);
1620
+ if (session !== watched || !session.established)
1621
+ return false;
1622
+ const start = session.sendBufferStartNum;
1623
+ if (start !== undefined) {
1624
+ // Wraparound-safe: the window is ≤8192 packets, so a "distance" over
1625
+ // 2^31 means start has passed endPacketNumber.
1626
+ const pending = (endPacketNumber - start) >>> 0;
1627
+ if (pending === 0 || pending > 0x80000000)
1628
+ return true;
1629
+ }
1630
+ else if (!session.sendArray || session.sendArray.size === 0) {
1631
+ return true; // nothing left unacknowledged at all
1632
+ }
1633
+ if (Date.now() >= deadline)
1634
+ return false;
1635
+ await sleep(250);
1636
+ }
1637
+ }
1638
+ /**
1639
+ * True when this friend is a NATIVE Carrier client (iOS/Android/C SDK): its
1640
+ * DHT/relay key differs from its identity key, where a JS peer announces its
1641
+ * identity key as its DHT key. Natives cannot see the toxcore file-transfer
1642
+ * protocol (sendFile) — the inline envelope is their only file path — and
1643
+ * they never speak the JS text-ACK scheme.
1644
+ */
1645
+ isNativeFriend(pubkey) {
1646
+ const friend = this.#friends.get(pubkey);
1647
+ return !!(friend?.dhtPubkey && friend.dhtPubkey !== friend.pubkey);
1572
1648
  }
1573
1649
  /** Fired when a peer sends a Carrier friend-invite (PACKET_TYPE_INVITE_REQUEST).
1574
1650
  * This is the channel the iOS/Android WebRTC SDK uses for call signaling —
@@ -135,6 +135,23 @@ export type TextMessage = {
135
135
  * can see when online delivery is failing and only offline messages land. */
136
136
  via?: "online" | "offline";
137
137
  };
138
+ /**
139
+ * Outcome of an inline-file send — the caller's ONLY honest basis for a "sent"
140
+ * checkmark. "Bytes left this machine" is not delivery.
141
+ */
142
+ export type InlineSendResult = {
143
+ /**
144
+ * - "acked": the peer CONFIRMED receipt — an app-level ACK from a JS peer,
145
+ * or the toxcore transport ACK from a native one (the same signal Android
146
+ * surfaces as ReceiptState.Delivered). Safe to mark "sent".
147
+ * - "accepted": the transport took the bytes but no confirmation arrived in
148
+ * the window. NOT delivered as far as anyone can prove — mark it failed or
149
+ * queued, never "sent".
150
+ * - "offline": stored on the express relay for the peer's next pull. Mark
151
+ * it queued.
152
+ */
153
+ delivery: "acked" | "accepted" | "offline";
154
+ };
138
155
  export type SendTextUntilAckOptions = {
139
156
  /** Stable id for retries across a caller-managed outbox. Defaults to a random id. */
140
157
  deliveryId?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/peer",
3
- "version": "0.1.139",
3
+ "version": "0.1.140",
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",