@decentnetwork/peer 0.1.0 → 0.1.2

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/peer.d.ts CHANGED
@@ -41,5 +41,36 @@ export declare class Peer {
41
41
  onFriendConnection(cb: (ev: FriendConnectionEvent) => void): void;
42
42
  onFriendInfo(cb: (ev: FriendInfoEvent) => void): void;
43
43
  friends(): FriendRecord[];
44
+ /**
45
+ * Read-only snapshot of the live net_crypto session state for a
46
+ * friend, or null if no session has been established yet. Lets
47
+ * callers see whether a peer is reachable via direct UDP, only via
48
+ * TCP relay, or not at all — the answer is the difference between
49
+ * ~80ms RTT (UDP) and ~500ms+ RTT (relay) in practice, so the
50
+ * caller (e.g. `agentnet diag`) can show an operator where their
51
+ * latency is coming from. Returns:
52
+ *
53
+ * established — handshake complete on this side
54
+ * udpRemote — direct UDP endpoint we've seen the peer at,
55
+ * if any; null means UDP holepunch hasn't
56
+ * succeeded (yet) so traffic falls back to TCP
57
+ * relay
58
+ * hasTcpRoute — true when a TCP relay has reported a route to
59
+ * the peer
60
+ * transport — convenience derived field: which path the next
61
+ * outbound packet will actually use
62
+ * lastPingRecvMs — for staleness; if older than ~32s the session
63
+ * is on its way to timeout
64
+ */
65
+ sessionStatus(pubkey: string): {
66
+ established: boolean;
67
+ udpRemote: {
68
+ host: string;
69
+ port: number;
70
+ } | null;
71
+ hasTcpRoute: boolean;
72
+ transport: "udp" | "tcp-relay" | "both" | "none";
73
+ lastPingRecvMs: number | null;
74
+ } | null;
44
75
  waitForFriendRequest(timeoutMs?: number): Promise<FriendRequest>;
45
76
  }
package/dist/peer.js CHANGED
@@ -747,6 +747,52 @@ export class Peer {
747
747
  friends() {
748
748
  return [...this.#friends.values()];
749
749
  }
750
+ /**
751
+ * Read-only snapshot of the live net_crypto session state for a
752
+ * friend, or null if no session has been established yet. Lets
753
+ * callers see whether a peer is reachable via direct UDP, only via
754
+ * TCP relay, or not at all — the answer is the difference between
755
+ * ~80ms RTT (UDP) and ~500ms+ RTT (relay) in practice, so the
756
+ * caller (e.g. `agentnet diag`) can show an operator where their
757
+ * latency is coming from. Returns:
758
+ *
759
+ * established — handshake complete on this side
760
+ * udpRemote — direct UDP endpoint we've seen the peer at,
761
+ * if any; null means UDP holepunch hasn't
762
+ * succeeded (yet) so traffic falls back to TCP
763
+ * relay
764
+ * hasTcpRoute — true when a TCP relay has reported a route to
765
+ * the peer
766
+ * transport — convenience derived field: which path the next
767
+ * outbound packet will actually use
768
+ * lastPingRecvMs — for staleness; if older than ~32s the session
769
+ * is on its way to timeout
770
+ */
771
+ sessionStatus(pubkey) {
772
+ const s = this.#friendSessions.get(pubkey);
773
+ if (!s)
774
+ return null;
775
+ // session.remote can be a synthetic `tcp:<dhtpk>:0` placeholder
776
+ // when the TCP relay path reports an endpoint with no real UDP
777
+ // address; treat those as no-UDP for reporting.
778
+ const realUdp = s.remote && !s.remote.host?.startsWith("tcp:") && s.remote.port !== 0
779
+ ? { host: s.remote.host, port: s.remote.port }
780
+ : null;
781
+ const transport = realUdp
782
+ ? s.hasTcpRoute
783
+ ? "both"
784
+ : "udp"
785
+ : s.hasTcpRoute
786
+ ? "tcp-relay"
787
+ : "none";
788
+ return {
789
+ established: s.established === true,
790
+ udpRemote: realUdp,
791
+ hasTcpRoute: s.hasTcpRoute === true,
792
+ transport,
793
+ lastPingRecvMs: s.lastPingRecvMs ?? null,
794
+ };
795
+ }
750
796
  waitForFriendRequest(timeoutMs = 30000) {
751
797
  return new Promise((resolve, reject) => {
752
798
  const timer = setTimeout(() => {
@@ -1984,6 +2030,79 @@ export class Peer {
1984
2030
  if (session.remote && friend.status !== "online") {
1985
2031
  this.#setFriendOnline(friendId, session.remote.host, session.remote.port);
1986
2032
  }
2033
+ // Phase 1.2: keep retrying UDP holepunch even after the session
2034
+ // is already "established" via TCP relay. The toxcore-derived
2035
+ // logic above stops trying once anybody is reachable, which is
2036
+ // correct for messaging (text gets through) but disastrous for
2037
+ // IP-packet forwarding (latency stays at ~500-1500ms via the
2038
+ // relay detour instead of dropping to ~80ms once UDP works).
2039
+ //
2040
+ // We detect "on TCP-relay only" as: session.hasTcpRoute is set
2041
+ // AND session.remote is either null or the synthetic
2042
+ // {host: "tcp:<dhtpk>", port: 0} placeholder. In that state we
2043
+ // re-send our DHT-PK so the peer (re-)learns our UDP endpoint,
2044
+ // re-discover their endpoint via DHT, and trigger a fresh
2045
+ // #initiateSession that will send a UDP cookie request. If UDP
2046
+ // succeeds, #handleCryptoHandshake overwrites session.remote
2047
+ // with the real endpoint and #sendMessengerPacket picks UDP
2048
+ // over TCP from then on. Throttled to UDP_RETRY_INTERVAL_MS so
2049
+ // the loop's 250ms tick doesn't flood DHT lookups.
2050
+ const realUdpRemote = session.remote &&
2051
+ !session.remote.host?.startsWith("tcp:") &&
2052
+ session.remote.port !== 0;
2053
+ if (!realUdpRemote && session.hasTcpRoute) {
2054
+ const UDP_RETRY_INTERVAL_MS = 15_000;
2055
+ const lastTry = session.lastUdpRetryMs ?? 0;
2056
+ if (now - lastTry > UDP_RETRY_INTERVAL_MS) {
2057
+ session.lastUdpRetryMs = now;
2058
+ // Recompute friend's real public key for the DHT-PK push.
2059
+ // Same fallback chain as the cooldown branch below.
2060
+ let friendRealPk = session.friendRealPublicKey;
2061
+ if (!friendRealPk && friend.address) {
2062
+ try {
2063
+ friendRealPk = parseCarrierAddress(friend.address).publicKey;
2064
+ }
2065
+ catch {
2066
+ // Fall through to the userid form.
2067
+ }
2068
+ }
2069
+ if (!friendRealPk && friend.pubkey) {
2070
+ try {
2071
+ friendRealPk = base58ToBytes(friend.pubkey);
2072
+ }
2073
+ catch {
2074
+ // Ignore malformed.
2075
+ }
2076
+ }
2077
+ if (friendRealPk && friendRealPk.length === 32) {
2078
+ // 1. Re-announce our DHT-PK so peer learns *our* UDP endpoint
2079
+ // even if their own DHT lookup against us was stale.
2080
+ void this.#sendOnionDhtPk(friendRealPk).catch((error) => {
2081
+ this.#debugLog(`UDP retry: dhtpk_send for ${friendId} failed: ${error.message}`);
2082
+ });
2083
+ // 2. Try to discover the peer's UDP endpoint and kick a fresh
2084
+ // cookie request via UDP. If it lands, session.remote
2085
+ // becomes a real UDP endpoint and the next outbound
2086
+ // packet goes direct.
2087
+ const dhtPk = session.friendDhtPublicKey;
2088
+ if (dhtPk) {
2089
+ void this.#discoverAndCacheFriendEndpoint(friendId, dhtPk)
2090
+ .then((found) => {
2091
+ if (found) {
2092
+ return this.#initiateSession(friendId).catch(() => undefined);
2093
+ }
2094
+ })
2095
+ .catch(() => undefined);
2096
+ }
2097
+ else if (friend.remoteHost && friend.remotePort) {
2098
+ // We already have a real UDP endpoint cached — just
2099
+ // retry the cookie request to it.
2100
+ void this.#initiateSession(friendId).catch(() => undefined);
2101
+ }
2102
+ this.#debugLog(`UDP retry attempt for ${friendId} (currently on TCP relay)`);
2103
+ }
2104
+ }
2105
+ }
1987
2106
  continue;
1988
2107
  }
1989
2108
  // No active session: tell the friend our DHT public key so they can
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/peer",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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",