@decentnetwork/peer 0.1.157 → 0.1.159

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
@@ -97,7 +97,28 @@ export declare class Peer {
97
97
  routes: number;
98
98
  targets: number;
99
99
  } | undefined;
100
- acceptFriendRequest(pubkey: string): Promise<void>;
100
+ /**
101
+ * Accept a friend request.
102
+ *
103
+ * `#pendingFriendRequests` is an in-memory cache, not the authority on what
104
+ * was asked. A host that remembers requests across a restart — beagle-web
105
+ * keeps them in IndexedDB, because a request usually arrives while the tab is
106
+ * closed — is doing the right thing, and refusing it here made every restored
107
+ * card permanently un-acceptable: click Accept, the SDK throws "no pending
108
+ * friend request", the card comes back, and the button reads as dead. That is
109
+ * the whole of the reported "Accept does not respond" on app.beagle.chat.
110
+ *
111
+ * Accepting is the host's decision. Take it, and let the host pass back what
112
+ * it remembered so the friend does not arrive nameless — anything missing is
113
+ * filled in by the userinfo exchange once the session comes up.
114
+ */
115
+ acceptFriendRequest(pubkey: string, opts?: {
116
+ name?: string;
117
+ description?: string;
118
+ hello?: string;
119
+ address?: string;
120
+ nospam?: number;
121
+ }): Promise<void>;
101
122
  /** Decline an inbound request.
102
123
  *
103
124
  * Also drops any friend record and session the request already created.
package/dist/peer.js CHANGED
@@ -103,6 +103,10 @@ const FAULT_REQUEST_HANG = process.env.DECENT_FAULT_REQUEST_HANG === "1";
103
103
  // toxcore expires announce entries on roughly this timescale, so a node that
104
104
  // acknowledged a store longer ago than this can no longer be counted on.
105
105
  const ANNOUNCE_ENTRY_TTL_MS = readEnvInt("DECENT_ANNOUNCE_ENTRY_TTL_MS", 300_000);
106
+ // How long a partial BULKMSG may go with no new fragment before it is
107
+ // abandoned. Refreshed on every fragment, so it bounds a STALLED transfer, not
108
+ // a large one.
109
+ const BULK_ASSEMBLY_IDLE_MS = readEnvInt("DECENT_BULK_ASSEMBLY_IDLE_MS", 60_000);
106
110
  const FAULT_ANNOUNCE_HANG_RUN = readEnvInt("DECENT_FAULT_ANNOUNCE_HANG", 0);
107
111
  // Classic-DHT maintenance cadence. Every tick we get_nodes toward our own key
108
112
  // (so neighbours store our address and native peers can find our UDP endpoint)
@@ -1245,20 +1249,49 @@ export class Peer {
1245
1249
  lastFriendRequestDispatch() {
1246
1250
  return this.#lastFriendRequestDispatch;
1247
1251
  }
1248
- async acceptFriendRequest(pubkey) {
1252
+ /**
1253
+ * Accept a friend request.
1254
+ *
1255
+ * `#pendingFriendRequests` is an in-memory cache, not the authority on what
1256
+ * was asked. A host that remembers requests across a restart — beagle-web
1257
+ * keeps them in IndexedDB, because a request usually arrives while the tab is
1258
+ * closed — is doing the right thing, and refusing it here made every restored
1259
+ * card permanently un-acceptable: click Accept, the SDK throws "no pending
1260
+ * friend request", the card comes back, and the button reads as dead. That is
1261
+ * the whole of the reported "Accept does not respond" on app.beagle.chat.
1262
+ *
1263
+ * Accepting is the host's decision. Take it, and let the host pass back what
1264
+ * it remembered so the friend does not arrive nameless — anything missing is
1265
+ * filled in by the userinfo exchange once the session comes up.
1266
+ */
1267
+ async acceptFriendRequest(pubkey, opts) {
1249
1268
  const request = this.#pendingFriendRequests.get(pubkey);
1250
- if (!request) {
1251
- throw new Error("No pending friend request from this peer");
1269
+ if (!request && !opts) {
1270
+ // No cached request AND the caller told us nothing: it can still be
1271
+ // accepted, but say so, because it means the host is not passing on what
1272
+ // it knows and the friend will start out anonymous.
1273
+ this.#debugLog(`accepting ${pubkey} with no cached request and no details from the host`);
1274
+ }
1275
+ let key;
1276
+ try {
1277
+ key = base58ToBytes(pubkey);
1278
+ }
1279
+ catch {
1280
+ throw new Error(`Not a valid peer key: ${pubkey}`);
1252
1281
  }
1282
+ if (key.length !== 32) {
1283
+ throw new Error(`Not a valid peer key: ${pubkey}`);
1284
+ }
1285
+ const nospam = request?.nospam ?? opts?.nospam ?? 0;
1253
1286
  this.#pendingFriendRequests.delete(pubkey);
1254
1287
  this.#friends.set(pubkey, {
1255
1288
  pubkey,
1256
- userid: request.userid,
1257
- address: request.address,
1258
- nospam: request.nospam,
1259
- name: request.name,
1260
- description: request.description,
1261
- hello: request.hello,
1289
+ userid: request?.userid ?? pubkey,
1290
+ address: request?.address ?? opts?.address ?? carrierAddressFromPublicKey(key, nospam),
1291
+ nospam,
1292
+ name: request?.name ?? opts?.name ?? "",
1293
+ description: request?.description ?? opts?.description ?? "",
1294
+ hello: request?.hello ?? opts?.hello ?? "",
1262
1295
  status: "offline",
1263
1296
  acceptedAt: Date.now()
1264
1297
  });
@@ -4797,10 +4830,25 @@ export class Peer {
4797
4830
  * lost fragment just expires this one message after 60s, nothing else). */
4798
4831
  #assembleBulkMsg(friendId, frag, packetNumber) {
4799
4832
  const now = Date.now();
4800
- // Lazy expiry sweep (C uses a 60s assembly timeout).
4833
+ // Lazy expiry sweep. The deadline is IDLE time, not total time.
4834
+ //
4835
+ // It used to be set once, at creation, and never extended — so an assembly
4836
+ // was thrown away 60s after its first fragment no matter how well it was
4837
+ // going. A phone photo is several megabytes; base64 makes it larger still,
4838
+ // and over a TCP relay that takes well over a minute. The transfer was
4839
+ // discarded mid-flight, with every fragment still arriving, and nothing was
4840
+ // logged. Small files were unaffected, which is why this survived testing:
4841
+ // a 300 KB file completes in seconds and never reaches the deadline.
4842
+ //
4843
+ // Idle is the right measure — give up when the SENDER stops, not when the
4844
+ // file is big. Unbounded growth is still prevented by the 16 MB size cap
4845
+ // that totalsz is checked against below.
4801
4846
  for (const [key, entry] of this.#bulkAssembly) {
4802
- if (entry.expireAtMs < now)
4847
+ if (entry.expireAtMs < now) {
4848
+ this.#debugLog(`bulkmsg ${key} abandoned: no fragment for ${Math.round(BULK_ASSEMBLY_IDLE_MS / 1000)}s ` +
4849
+ `(had ${entry.got}/${entry.total} bytes)`);
4803
4850
  this.#bulkAssembly.delete(key);
4851
+ }
4804
4852
  }
4805
4853
  const key = `${friendId}:${frag.tid.toString()}`;
4806
4854
  // A completed assembly is deleted, so a second copy of the same fragments
@@ -4823,7 +4871,7 @@ export class Peer {
4823
4871
  }
4824
4872
  let entry = this.#bulkAssembly.get(key);
4825
4873
  if (!entry) {
4826
- entry = { total: 0, frags: new Map(), got: 0, expireAtMs: now + 60_000 };
4874
+ entry = { total: 0, frags: new Map(), got: 0, expireAtMs: now + BULK_ASSEMBLY_IDLE_MS };
4827
4875
  this.#bulkAssembly.set(key, entry);
4828
4876
  }
4829
4877
  // The first fragment carries totalsz; it may arrive out of order.
@@ -4837,6 +4885,9 @@ export class Peer {
4837
4885
  }
4838
4886
  // Store by packet number; a duplicate (dual-send) fragment is ignored.
4839
4887
  if (frag.data.length > 0 && !entry.frags.has(packetNumber)) {
4888
+ // Progress: push the idle deadline out. This is what makes the timeout
4889
+ // mean "the sender went away" instead of "the file was too big".
4890
+ entry.expireAtMs = now + BULK_ASSEMBLY_IDLE_MS;
4840
4891
  entry.frags.set(packetNumber, frag.data);
4841
4892
  entry.got += frag.data.length;
4842
4893
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/peer",
3
- "version": "0.1.157",
3
+ "version": "0.1.159",
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",