@decentnetwork/peer 0.1.141 → 0.1.143

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
@@ -335,11 +335,13 @@ export declare class Peer {
335
335
  setUserInfo(info: {
336
336
  name?: string;
337
337
  description?: string;
338
+ punkId?: number | null;
338
339
  }): void;
339
340
  /** Our current display name + status-message description. */
340
341
  userInfo(): {
341
342
  name: string;
342
343
  description: string;
344
+ punkId?: number;
343
345
  };
344
346
  /**
345
347
  * Tell a friend our public UDP endpoint over the (already-working,
package/dist/peer.js CHANGED
@@ -286,6 +286,19 @@ const RECV_REORDER_WINDOW = 8192;
286
286
  // Minimum gap between PACKET_ID_REQUEST packets we send for our own receive gaps
287
287
  // (toxcore paces requests ~ once per RTT; this floor avoids flooding on a burst).
288
288
  const RECV_REQUEST_MIN_INTERVAL_MS = 200;
289
+ /** Read a CryptoPunks id out of the userinfo `gender` field.
290
+ *
291
+ * Beagle writes the bare nft id there. Anything else — an empty string, or a
292
+ * real gender from a non-Beagle Carrier client — is not a punk and must not
293
+ * be rendered as one. */
294
+ function parsePunkField(value) {
295
+ if (!value)
296
+ return undefined;
297
+ const n = Number(value.trim());
298
+ if (!Number.isInteger(n) || n < 0 || n > 9999)
299
+ return undefined;
300
+ return n;
301
+ }
289
302
  export class Peer {
290
303
  #opts;
291
304
  #events = new EventEmitter();
@@ -885,6 +898,41 @@ export class Peer {
885
898
  if (!this.#bootstrap) {
886
899
  throw new Error("Peer is not started");
887
900
  }
901
+ // A browser has no UDP and never will. The DHT bootstrap is a getnodes
902
+ // datagram waiting for a sendnodes reply, so there it can only ever time
903
+ // out — 30 seconds, then an error that reads like a network fault.
904
+ //
905
+ // That cost far more than a slow start. Everything after the await lives
906
+ // downstream of it: the self-announce (without which nobody can FIND this
907
+ // peer), the announce loop, and the express pull loop (without which
908
+ // offline messages are never collected). start() launches only the
909
+ // friend-connection loop, so a browser could talk to a friend who was
910
+ // already online and whose endpoint it had cached — and nothing else.
911
+ // That is exactly the half-working shape this looked like from outside.
912
+ //
913
+ // With tcpOnlyBootstrap we skip the datagram round trip and seed the node
914
+ // list from the configured bootstrap nodes, which is what the TCP relay
915
+ // path uses anyway. Announce and both loops then run normally.
916
+ if (this.#opts.tcpOnlyBootstrap) {
917
+ const seed = this.#opts.bootstrapNodes ?? [];
918
+ if (seed.length === 0) {
919
+ throw new Error("tcpOnlyBootstrap needs at least one bootstrap node to seed from");
920
+ }
921
+ this.#knownNodes = dedupeNodes([...seed]);
922
+ await this.#runSelfAnnounce(false, Date.now() + JOIN_ANNOUNCE_TIMEOUT_MS);
923
+ this.#ensureSelfAnnounceLoop();
924
+ this.#ensureDhtMaintenanceLoop();
925
+ this.#ensureFriendConnectionLoop();
926
+ this.#ensureExpressPullLoop();
927
+ void this.#doFriendConnections().catch((error) => {
928
+ this.#debugLog(`initial friend connection cycle failed: ${error.message}`);
929
+ });
930
+ const first = seed[0];
931
+ return {
932
+ respondingNode: first,
933
+ discoveredNodes: []
934
+ };
935
+ }
888
936
  const result = await this.#bootstrap.join();
889
937
  this.#recordNodeSuccess(`${result.respondingNode.host}:${result.respondingNode.port}`);
890
938
  const discovered = result.discoveredNodes.map((node) => ({
@@ -4250,12 +4298,17 @@ export class Peer {
4250
4298
  // back to plain UTF-8 for compatibility.
4251
4299
  let userInfoName;
4252
4300
  let userInfoDescr;
4301
+ let userInfoPunk;
4253
4302
  let clientMeta;
4254
4303
  try {
4255
4304
  const decoded = decodeCarrierPacket(inner);
4256
4305
  if (decoded.type === PACKET_TYPE_USERINFO) {
4257
4306
  userInfoName = decoded.name;
4258
4307
  userInfoDescr = decoded.descr;
4308
+ // The friend's avatar. `gender` is where Beagle puts the punk id —
4309
+ // it was decoded here and thrown away, which is the whole reason a
4310
+ // friend's picture could only ever come from beagles.eth.
4311
+ userInfoPunk = parsePunkField(decoded.gender);
4259
4312
  // AgentNet appended fields — present only for updated peers.
4260
4313
  if (decoded.protoVersion || decoded.platform || decoded.appVersion) {
4261
4314
  clientMeta = {
@@ -4274,19 +4327,23 @@ export class Peer {
4274
4327
  const friend = this.#friends.get(friendId);
4275
4328
  const newName = userInfoName && userInfoName.length > 0 ? userInfoName : friend?.name;
4276
4329
  const newDescr = userInfoDescr ?? friend?.description;
4330
+ // Keep what we had when this packet carried no punk: a peer that
4331
+ // stops sending one has not taken their avatar off.
4332
+ const newPunk = userInfoPunk ?? friend?.punkId;
4277
4333
  const metaChanged = friend != null && clientMeta != null &&
4278
4334
  (friend.protoVersion !== clientMeta.protoVersion ||
4279
4335
  friend.platform !== clientMeta.platform ||
4280
4336
  friend.osVersion !== clientMeta.osVersion ||
4281
4337
  friend.appVersion !== clientMeta.appVersion);
4282
- if (friend && (friend.name !== newName || friend.description !== newDescr || metaChanged)) {
4283
- this.#friends.set(friendId, { ...friend, name: newName, description: newDescr, ...(clientMeta ?? {}) });
4338
+ if (friend && (friend.name !== newName || friend.description !== newDescr || friend.punkId !== newPunk || metaChanged)) {
4339
+ this.#friends.set(friendId, { ...friend, name: newName, description: newDescr, punkId: newPunk, ...(clientMeta ?? {}) });
4284
4340
  this.#persistFriends();
4285
4341
  this.#events.emit("friendInfo", {
4286
4342
  pubkey: friendId,
4287
4343
  userid: friend.userid ?? friendId,
4288
4344
  name: newName,
4289
4345
  description: newDescr,
4346
+ punkId: newPunk,
4290
4347
  ...(clientMeta ?? {})
4291
4348
  });
4292
4349
  }
@@ -4958,6 +5015,11 @@ export class Peer {
4958
5015
  opts.nickname = info.name;
4959
5016
  if (info.description !== undefined)
4960
5017
  opts.statusMessage = info.description;
5018
+ // null clears it explicitly; undefined means "not changing it". Everything
5019
+ // below re-sends the profile to every established friend, which is what
5020
+ // makes an avatar change visible without a reconnect.
5021
+ if (info.punkId !== undefined)
5022
+ opts.punkId = info.punkId ?? undefined;
4961
5023
  this.#profileSentTo.clear();
4962
5024
  for (const timer of this.#profileRetryTimers.values())
4963
5025
  clearTimeout(timer);
@@ -4974,6 +5036,9 @@ export class Peer {
4974
5036
  return {
4975
5037
  name: opts.nickname ?? PEER_NICKNAME,
4976
5038
  description: opts.statusMessage ?? PEER_STATUS_MESSAGE,
5039
+ // Reported so "what am I actually advertising" is answerable without
5040
+ // reading a packet capture — the UI surfaces this as `me.advertised`.
5041
+ punkId: opts.punkId,
4977
5042
  };
4978
5043
  }
4979
5044
  #scheduleProfileRetry(friendId) {
@@ -5030,7 +5095,12 @@ export class Peer {
5030
5095
  protoVersion: AGENTNET_PROTO_VERSION,
5031
5096
  platform: this.#opts.platform ?? process.platform,
5032
5097
  osVersion: this.#opts.osVersion ?? `node-${process.versions?.node ?? ""}`,
5033
- appVersion: this.#opts.appVersion ?? `peer-${PEER_PKG_VERSION}`
5098
+ appVersion: this.#opts.appVersion ?? `peer-${PEER_PKG_VERSION}`,
5099
+ // The avatar, in the field Beagle uses for it (see punkId on
5100
+ // FriendInfoEvent). OMITTED when we have none, never sent as "":
5101
+ // an empty string would clear an avatar the friend already
5102
+ // resolved. Without this, iOS shows us no picture at all.
5103
+ ...(this.#opts.punkId != null ? { gender: String(this.#opts.punkId) } : {})
5034
5104
  });
5035
5105
  await this.#sendMessengerPacket(friendId, PACKET_ID_STATUSMESSAGE, userInfo);
5036
5106
  this.#debugLog(`userinfo sent to ${friendId} (name="${nick}", descr="${descr}", proto=${AGENTNET_PROTO_VERSION}, platform=${this.#opts.platform ?? process.platform})`);
@@ -5,6 +5,10 @@ export type FriendRecord = {
5
5
  nospam?: number;
6
6
  name?: string;
7
7
  description?: string;
8
+ /** The friend's CryptoPunks id, from the userinfo `gender` field. Persisted
9
+ * so their avatar survives a restart instead of waiting for the next
10
+ * profile packet. See FriendInfoEvent.punkId. */
11
+ punkId?: number;
8
12
  status: "requested" | "offline" | "online";
9
13
  remoteHost?: string;
10
14
  remotePort?: number;
@@ -88,6 +88,18 @@ export type PeerOptions = {
88
88
  * version.
89
89
  */
90
90
  appVersion?: string;
91
+ /** Our own CryptoPunks id, advertised to friends in the userinfo `gender`
92
+ * field — the only way an avatar travels over Carrier itself. Leave unset
93
+ * and the field is omitted entirely (never sent as ""), so a friend keeps
94
+ * whatever picture they already have for us. */
95
+ punkId?: number;
96
+ /** Skip the UDP DHT bootstrap and seed the node list from bootstrapNodes.
97
+ *
98
+ * For hosts with no UDP — a browser above all. Without it joinNetwork()
99
+ * waits 30s for a datagram that cannot arrive, and everything downstream
100
+ * of it never runs: self-announce (so nobody can find this peer) and the
101
+ * express pull loop (so offline messages are never collected). */
102
+ tcpOnlyBootstrap?: boolean;
91
103
  compatibilityMode?: CompatibilityMode;
92
104
  debugLabel?: string;
93
105
  };
@@ -110,6 +122,18 @@ export type FriendInfoEvent = {
110
122
  userid?: string;
111
123
  name?: string;
112
124
  description?: string;
125
+ /** The friend's CryptoPunks id — their avatar.
126
+ *
127
+ * Carried in the userinfo packet's `gender` field, which is not a mistake
128
+ * and not ours to rename. The protocol has no avatar field: `has_avatar`
129
+ * is a bool in all four implementations and the image exchange was never
130
+ * built. Beagle borrowed `gender` because a punk carries a gender anyway.
131
+ * Defined in WalletForBeagleV2/docs/profile.md — set with
132
+ * userinfo.setGender(nftid), read back off friendInfo.gender.
133
+ *
134
+ * Undefined when the friend sent no punk, or sent something that is not a
135
+ * punk id (a real gender from a non-Beagle Carrier client). */
136
+ punkId?: number;
113
137
  /** Peer's advertised client metadata (from the userinfo profile). Present
114
138
  * only once the friend has sent a profile with these fields — a legacy
115
139
  * peer (native C SDK before the extension, or an old JS build) leaves them
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/peer",
3
- "version": "0.1.141",
3
+ "version": "0.1.143",
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",