@decentnetwork/peer 0.1.93 → 0.1.95

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 +64 -4
  2. package/package.json +1 -1
package/dist/peer.js CHANGED
@@ -358,6 +358,8 @@ export class Peer {
358
358
  // Per-friend tracking sets so we send our nickname/status/greeting once
359
359
  // per session rather than on every PACKET_ID_ONLINE arrival.
360
360
  #profileSentTo = new Set();
361
+ #profileRetryAttempts = new Map();
362
+ #profileRetryTimers = new Map();
361
363
  #greetingSentTo = new Set();
362
364
  #selfAnnouncePromise;
363
365
  #selfAnnouncePauseDepth = 0;
@@ -595,6 +597,10 @@ export class Peer {
595
597
  async stop() {
596
598
  // Stop background producers first so no keepalive/profile packet can race
597
599
  // behind the shutdown notice and make the remote session look live again.
600
+ for (const timer of this.#profileRetryTimers.values())
601
+ clearTimeout(timer);
602
+ this.#profileRetryTimers.clear();
603
+ this.#profileRetryAttempts.clear();
598
604
  if (this.#expressPollTimer) {
599
605
  clearInterval(this.#expressPollTimer);
600
606
  this.#expressPollTimer = undefined;
@@ -981,6 +987,11 @@ export class Peer {
981
987
  this.#tcpOnlyWarningShown.delete(friendId);
982
988
  this.#noEndpointWarned.delete(friendId);
983
989
  this.#profileSentTo.delete(friendId);
990
+ const profileRetry = this.#profileRetryTimers.get(friendId);
991
+ if (profileRetry)
992
+ clearTimeout(profileRetry);
993
+ this.#profileRetryTimers.delete(friendId);
994
+ this.#profileRetryAttempts.delete(friendId);
984
995
  this.#greetingSentTo.delete(friendId);
985
996
  if (existed) {
986
997
  this.#persistFriends();
@@ -1760,10 +1771,19 @@ export class Peer {
1760
1771
  const now = Date.now();
1761
1772
  const peerSendingUndecryptable = state.undecryptableRecvMs !== undefined &&
1762
1773
  now - state.undecryptableRecvMs < FRIEND_TIMEOUT_MS;
1774
+ // A duplicate handshake can also be the first packet from a freshly
1775
+ // restarted peer whose KILL notice was lost during shutdown. The
1776
+ // holder still has the peer's old accepted session shell, while the
1777
+ // restarted process needs our handshake reply before it can derive the
1778
+ // shared key. Re-send our CURRENT handshake (same key, so no local
1779
+ // reset) even if we also initiated a cookie chain: after a blackout
1780
+ // both sides commonly initiate at once, so `weInitiated` cannot tell a
1781
+ // request from a reply here. The 2s rate limit bounds duplicate traffic;
1782
+ // the immediate reply cannot bounce because it arrives inside the
1783
+ // limit. This is also the normal lost-reply recovery path.
1763
1784
  const resyncDue = now - (state.lastResyncHandshakeMs ?? 0) > 2000;
1764
- if (peerSendingUndecryptable && resyncDue) {
1785
+ if (resyncDue) {
1765
1786
  state.lastResyncHandshakeMs = now;
1766
- state.lastPingRecvMs = now;
1767
1787
  const reply = createCryptoHandshake({
1768
1788
  recipientCookie: hs.embeddedCookie,
1769
1789
  baseNonce: state.ourBaseNonce,
@@ -1779,11 +1799,16 @@ export class Peer {
1779
1799
  ? Promise.resolve(this.#tcpRelays?.sendOobToFriend(hs.senderDhtPublicKey, reply) ?? 0)
1780
1800
  : this.#sendPacket(reply, { host: remote.address, port: remote.port });
1781
1801
  void sendResync()
1782
- .then(() => this.#debugLog(`hs_recv duplicate but peer sent undecryptable data — re-sent our handshake to resync friend=${friendId}`))
1802
+ .then(() => this.#debugLog(`hs_recv duplicate — re-sent our handshake to resync friend=${friendId} ` +
1803
+ `reason=${peerSendingUndecryptable ? "undecryptable-data" : "duplicate-handshake"}`))
1783
1804
  .catch((error) => this.#debugLog(`resync handshake failed for ${friendId}: ${error.message}`));
1784
1805
  return;
1785
1806
  }
1786
- state.lastPingRecvMs = now;
1807
+ // A handshake proves reachability, not that this net_crypto session can
1808
+ // decrypt data. Refreshing lastPingRecvMs here kept a stale session
1809
+ // immortal: the restarted peer retransmitted handshakes every second,
1810
+ // and each retransmit postponed the 32s dead-session breaker forever.
1811
+ // Only successfully decrypted CRYPTO_DATA may refresh liveness.
1787
1812
  this.#debugVerboseLog(`hs_recv duplicate friend=${friendId} remote=${remote.address}:${remote.port} (session preserved)`);
1788
1813
  return;
1789
1814
  }
@@ -3978,6 +4003,10 @@ export class Peer {
3978
4003
  if (info.description !== undefined)
3979
4004
  opts.statusMessage = info.description;
3980
4005
  this.#profileSentTo.clear();
4006
+ for (const timer of this.#profileRetryTimers.values())
4007
+ clearTimeout(timer);
4008
+ this.#profileRetryTimers.clear();
4009
+ this.#profileRetryAttempts.clear();
3981
4010
  for (const [friendId, s] of this.#friendSessions.entries()) {
3982
4011
  if (s.established)
3983
4012
  this.#sendProfileAndGreeting(friendId);
@@ -3991,6 +4020,25 @@ export class Peer {
3991
4020
  description: opts.statusMessage ?? PEER_STATUS_MESSAGE,
3992
4021
  };
3993
4022
  }
4023
+ #scheduleProfileRetry(friendId) {
4024
+ if (this.#profileRetryTimers.has(friendId))
4025
+ return;
4026
+ const session = this.#friendSessions.get(friendId);
4027
+ if (!this.#started || !session?.established)
4028
+ return;
4029
+ const attempt = (this.#profileRetryAttempts.get(friendId) ?? 0) + 1;
4030
+ this.#profileRetryAttempts.set(friendId, attempt);
4031
+ const delayMs = Math.min(30_000, 1_000 * (2 ** Math.min(attempt - 1, 5)));
4032
+ const timer = setTimeout(() => {
4033
+ this.#profileRetryTimers.delete(friendId);
4034
+ if (this.#started && this.#friendSessions.get(friendId)?.established) {
4035
+ this.#sendProfileAndGreeting(friendId);
4036
+ }
4037
+ }, delayMs);
4038
+ timer.unref?.();
4039
+ this.#profileRetryTimers.set(friendId, timer);
4040
+ this.#debugLog(`profile retry ${attempt} for ${friendId} scheduled in ${delayMs}ms`);
4041
+ }
3994
4042
  #sendProfileAndGreeting(friendId) {
3995
4043
  if (!this.#profileSentTo.has(friendId)) {
3996
4044
  this.#profileSentTo.add(friendId);
@@ -4030,9 +4078,16 @@ export class Peer {
4030
4078
  });
4031
4079
  await this.#sendMessengerPacket(friendId, PACKET_ID_STATUSMESSAGE, userInfo);
4032
4080
  this.#debugLog(`userinfo sent to ${friendId} (name="${nick}", descr="${descr}", proto=${AGENTNET_PROTO_VERSION}, platform=${this.#opts.platform ?? process.platform})`);
4081
+ this.#profileRetryAttempts.delete(friendId);
4033
4082
  }
4034
4083
  catch (error) {
4084
+ // PACKET_ID_ONLINE can arrive a fraction before every transport is
4085
+ // ready to accept messenger packets. Do not leave the optimistic
4086
+ // "sent" marker wedged after that race: re-arm it and retry with
4087
+ // bounded backoff while this same session remains established.
4088
+ this.#profileSentTo.delete(friendId);
4035
4089
  this.#debugLog(`send profile failed for ${friendId}: ${error.message}`);
4090
+ this.#scheduleProfileRetry(friendId);
4036
4091
  }
4037
4092
  })();
4038
4093
  }
@@ -5364,6 +5419,11 @@ export class Peer {
5364
5419
  // transitions prevents the user from seeing the same greeting message
5365
5420
  // repeated each time the session bounces.
5366
5421
  this.#profileSentTo.delete(friendId);
5422
+ const profileRetry = this.#profileRetryTimers.get(friendId);
5423
+ if (profileRetry)
5424
+ clearTimeout(profileRetry);
5425
+ this.#profileRetryTimers.delete(friendId);
5426
+ this.#profileRetryAttempts.delete(friendId);
5367
5427
  const friend = this.#friends.get(friendId);
5368
5428
  if (!friend) {
5369
5429
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/peer",
3
- "version": "0.1.93",
3
+ "version": "0.1.95",
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",