@decentnetwork/peer 0.1.155 → 0.1.157
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.js +106 -3
- package/package.json +1 -1
package/dist/peer.js
CHANGED
|
@@ -93,6 +93,12 @@ const SELF_ANNOUNCE_INTERVAL_MS = readEnvInt("DECENT_SELF_ANNOUNCE_INTERVAL_MS",
|
|
|
93
93
|
// abandoned run is left to finish or hang on its own; what matters is that the
|
|
94
94
|
// NEXT tick is allowed to start.
|
|
95
95
|
const SELF_ANNOUNCE_WATCHDOG_MARGIN_MS = readEnvInt("DECENT_SELF_ANNOUNCE_WATCHDOG_MARGIN_MS", 15000);
|
|
96
|
+
// Longest a self-announce pause may hold before it releases itself. The only
|
|
97
|
+
// caller wraps a single friend-request send, which has its own 8s announce
|
|
98
|
+
// deadline plus route discovery; 60s is well clear of a slow-but-working send
|
|
99
|
+
// and well short of "the peer is now invisible".
|
|
100
|
+
const SELF_ANNOUNCE_PAUSE_MAX_MS = readEnvInt("DECENT_SELF_ANNOUNCE_PAUSE_MAX_MS", 60_000);
|
|
101
|
+
const FAULT_REQUEST_HANG = process.env.DECENT_FAULT_REQUEST_HANG === "1";
|
|
96
102
|
// How long a stored announce entry stays useful to someone looking us up.
|
|
97
103
|
// toxcore expires announce entries on roughly this timescale, so a node that
|
|
98
104
|
// acknowledged a store longer ago than this can no longer be counted on.
|
|
@@ -521,6 +527,10 @@ export class Peer {
|
|
|
521
527
|
// #initiateSession every few seconds for every friend; without this
|
|
522
528
|
// dedupe, lower-pubkey side floods the log with "deferring to peer".
|
|
523
529
|
#initiateSkipLogged = new Set();
|
|
530
|
+
/** Recently COMPLETED bulkmsg assemblies, same key, kept for the same 60s
|
|
531
|
+
* the assembly itself lives. Without it a retransmitted message reassembles
|
|
532
|
+
* a second time and is delivered twice. */
|
|
533
|
+
#bulkCompleted = new Map();
|
|
524
534
|
// In-flight Carrier BULKMSG reassemblies, keyed `${friendId}:${tid}`.
|
|
525
535
|
// Fragments are appended in arrival order (the lossless stream keeps
|
|
526
536
|
// them ordered) and the message completes when totalsz bytes arrived.
|
|
@@ -1101,6 +1111,13 @@ export class Peer {
|
|
|
1101
1111
|
}
|
|
1102
1112
|
const resumeSelfAnnounce = this.#pauseSelfAnnounce();
|
|
1103
1113
|
try {
|
|
1114
|
+
// Fault injection, off unless DECENT_FAULT_REQUEST_HANG=1. Hangs inside
|
|
1115
|
+
// the paused region — the shape that killed self-announce in the
|
|
1116
|
+
// browser — so the pause expiry can be tested rather than argued for.
|
|
1117
|
+
if (FAULT_REQUEST_HANG) {
|
|
1118
|
+
this.#debugLog("FAULT: hanging inside the self-announce pause forever");
|
|
1119
|
+
await new Promise(() => { });
|
|
1120
|
+
}
|
|
1104
1121
|
if (this.#selfAnnouncePromise) {
|
|
1105
1122
|
await this.#selfAnnouncePromise.catch(() => { });
|
|
1106
1123
|
}
|
|
@@ -1600,6 +1617,29 @@ export class Peer {
|
|
|
1600
1617
|
return;
|
|
1601
1618
|
}
|
|
1602
1619
|
}
|
|
1620
|
+
// Inline files ride the TEXT channel, and the delivery envelope wraps them.
|
|
1621
|
+
//
|
|
1622
|
+
// The call sites check for an inline file BEFORE this function, i.e. before
|
|
1623
|
+
// the envelope comes off — so they were looking at "\x1eDNPACK1:<base64>",
|
|
1624
|
+
// which does not start with "{", and every inline file sent with delivery
|
|
1625
|
+
// tracking was reported as a text message instead. Measured 2026-08-31,
|
|
1626
|
+
// peer to peer: a 95-byte PNG arrived as 200 characters of JSON, and a
|
|
1627
|
+
// 300 KB one as 409,672 characters. Both transferred perfectly; both were
|
|
1628
|
+
// handed to the application as a wall of base64.
|
|
1629
|
+
//
|
|
1630
|
+
// The check belongs here, on the unwrapped text, where it sees what the
|
|
1631
|
+
// sender actually wrote. The call-site checks stay for envelope-less
|
|
1632
|
+
// senders (iOS and Android do not speak this envelope).
|
|
1633
|
+
//
|
|
1634
|
+
// ACK first: an inline file is delivered, and skipping the ACK because we
|
|
1635
|
+
// did not run the text handlers would make the sender retransmit forever.
|
|
1636
|
+
if (this.#tryEmitInlineFile(msg.pubkey, text, msg.via === "offline" ? "offline" : "online")) {
|
|
1637
|
+
if (deliveryId) {
|
|
1638
|
+
this.#rememberDeliveredTextId(deliveryId);
|
|
1639
|
+
void this.#sendTextAck(msg.pubkey, deliveryId).catch(() => undefined);
|
|
1640
|
+
}
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1603
1643
|
if (this.#textHandlers.size === 0) {
|
|
1604
1644
|
if (deliveryId)
|
|
1605
1645
|
this.#debugLog(`text delivery ${deliveryId} from ${msg.pubkey} has no onText handlers; not ACKing`);
|
|
@@ -3166,13 +3206,22 @@ export class Peer {
|
|
|
3166
3206
|
try {
|
|
3167
3207
|
const decoded = decodeCarrierPacket(carrierPacket);
|
|
3168
3208
|
if (decoded.type !== PACKET_TYPE_FRIEND_REQUEST) {
|
|
3209
|
+
// Was a bare `return`. An onion friend-request that arrived and did not
|
|
3210
|
+
// parse left NOTHING anywhere — the sender saw success, the recipient
|
|
3211
|
+
// saw no request, and there was no way to tell "never arrived" from
|
|
3212
|
+
// "arrived and was dropped". That is the single most-reported symptom
|
|
3213
|
+
// on this project and it was undiagnosable by construction.
|
|
3214
|
+
this.#debugLog(`friend-request from ${carrierIdFromPublicKey(senderPublicKey)} DROPPED: ` +
|
|
3215
|
+
`decoded as packet type ${decoded.type}, expected ${PACKET_TYPE_FRIEND_REQUEST}`);
|
|
3169
3216
|
return;
|
|
3170
3217
|
}
|
|
3171
3218
|
hello = decoded.hello;
|
|
3172
3219
|
name = decoded.name;
|
|
3173
3220
|
description = decoded.descr;
|
|
3174
3221
|
}
|
|
3175
|
-
catch {
|
|
3222
|
+
catch (error) {
|
|
3223
|
+
this.#debugLog(`friend-request from ${carrierIdFromPublicKey(senderPublicKey)} DROPPED: ` +
|
|
3224
|
+
`undecodable (${carrierPacket.length} bytes): ${error.message}`);
|
|
3176
3225
|
return;
|
|
3177
3226
|
}
|
|
3178
3227
|
const userid = carrierIdFromPublicKey(senderPublicKey);
|
|
@@ -3214,13 +3263,17 @@ export class Peer {
|
|
|
3214
3263
|
try {
|
|
3215
3264
|
const decoded = decodeCarrierPacket(packet);
|
|
3216
3265
|
if (decoded.type !== PACKET_TYPE_FRIEND_REQUEST) {
|
|
3266
|
+
this.#debugLog(`offline friend-request from ${fromUserId} DROPPED: decoded as packet ` +
|
|
3267
|
+
`type ${decoded.type}, expected ${PACKET_TYPE_FRIEND_REQUEST}`);
|
|
3217
3268
|
return;
|
|
3218
3269
|
}
|
|
3219
3270
|
helloText = decoded.hello;
|
|
3220
3271
|
name = decoded.name;
|
|
3221
3272
|
description = decoded.descr;
|
|
3222
3273
|
}
|
|
3223
|
-
catch {
|
|
3274
|
+
catch (error) {
|
|
3275
|
+
this.#debugLog(`offline friend-request from ${fromUserId} DROPPED: undecodable ` +
|
|
3276
|
+
`(${packet.length} bytes): ${error.message}`);
|
|
3224
3277
|
return;
|
|
3225
3278
|
}
|
|
3226
3279
|
// Already an established friend? Ignore this (re-)request. Auto-friend
|
|
@@ -4750,6 +4803,24 @@ export class Peer {
|
|
|
4750
4803
|
this.#bulkAssembly.delete(key);
|
|
4751
4804
|
}
|
|
4752
4805
|
const key = `${friendId}:${frag.tid.toString()}`;
|
|
4806
|
+
// A completed assembly is deleted, so a second copy of the same fragments
|
|
4807
|
+
// starts a fresh entry and completes AGAIN — the application gets the same
|
|
4808
|
+
// message twice. Duplicate FRAGMENTS were already ignored; a duplicate
|
|
4809
|
+
// whole MESSAGE was not.
|
|
4810
|
+
//
|
|
4811
|
+
// It shows up on the plain send path (a native peer, or a JS peer before
|
|
4812
|
+
// userinfo has settled), because that path carries no delivery id and the
|
|
4813
|
+
// text-level dedupe has nothing to key on. Measured 2026-08-31: one
|
|
4814
|
+
// sendInlineFile call, two `bulkmsg complete (409672 bytes)` and two
|
|
4815
|
+
// identical 300 KB images delivered.
|
|
4816
|
+
for (const [k, at] of this.#bulkCompleted) {
|
|
4817
|
+
if (at < now - 60_000)
|
|
4818
|
+
this.#bulkCompleted.delete(k);
|
|
4819
|
+
}
|
|
4820
|
+
if (this.#bulkCompleted.has(key)) {
|
|
4821
|
+
this.#debugLog(`bulkmsg ${key} already completed — dropping duplicate`);
|
|
4822
|
+
return undefined;
|
|
4823
|
+
}
|
|
4753
4824
|
let entry = this.#bulkAssembly.get(key);
|
|
4754
4825
|
if (!entry) {
|
|
4755
4826
|
entry = { total: 0, frags: new Map(), got: 0, expireAtMs: now + 60_000 };
|
|
@@ -4774,6 +4845,7 @@ export class Peer {
|
|
|
4774
4845
|
if (entry.total === 0 || entry.got < entry.total)
|
|
4775
4846
|
return undefined;
|
|
4776
4847
|
this.#bulkAssembly.delete(key);
|
|
4848
|
+
this.#bulkCompleted.set(key, now);
|
|
4777
4849
|
if (entry.got > entry.total) {
|
|
4778
4850
|
this.#debugLog(`bulkmsg from ${friendId} got ${entry.got} > total ${entry.total} — dropped`);
|
|
4779
4851
|
return undefined;
|
|
@@ -7347,11 +7419,42 @@ export class Peer {
|
|
|
7347
7419
|
const recentBonus = Date.now() - h.lastOkMs < 60_000 ? 2 : 0;
|
|
7348
7420
|
return (h.ok * 2) - h.fail + recentBonus;
|
|
7349
7421
|
}
|
|
7422
|
+
/**
|
|
7423
|
+
* Pause the self-announce loop, with an expiry.
|
|
7424
|
+
*
|
|
7425
|
+
* The pause is released in a `finally`, which is correct right up until the
|
|
7426
|
+
* guarded body never finishes. sendFriendRequest awaits a relay send inside
|
|
7427
|
+
* that body, and in a browser a backgrounded tab can leave that socket
|
|
7428
|
+
* neither open nor errored — so the `finally` never runs, the depth stays at
|
|
7429
|
+
* 1, and self-announce is dead forever.
|
|
7430
|
+
*
|
|
7431
|
+
* This latch sits BEFORE the watchdog in #ensureSelfAnnounceLoop: the tick
|
|
7432
|
+
* returns on pause depth without ever calling #runSelfAnnounce, so bounding
|
|
7433
|
+
* the run was not enough on its own. Measured on app.beagle.chat after the
|
|
7434
|
+
* watchdog shipped: announce age still climbing past 700s while the page's
|
|
7435
|
+
* other timers ran normally.
|
|
7436
|
+
*
|
|
7437
|
+
* A pause is a short-lived thing — a few seconds around one send. One that
|
|
7438
|
+
* outlives this window is a bug somewhere upstream, and the loop matters
|
|
7439
|
+
* more than the pause does.
|
|
7440
|
+
*/
|
|
7350
7441
|
#pauseSelfAnnounce() {
|
|
7351
7442
|
this.#selfAnnouncePauseDepth += 1;
|
|
7352
|
-
|
|
7443
|
+
let released = false;
|
|
7444
|
+
const release = (viaTimeout) => {
|
|
7445
|
+
if (released)
|
|
7446
|
+
return;
|
|
7447
|
+
released = true;
|
|
7448
|
+
clearTimeout(expiry);
|
|
7353
7449
|
this.#selfAnnouncePauseDepth = Math.max(0, this.#selfAnnouncePauseDepth - 1);
|
|
7450
|
+
if (viaTimeout) {
|
|
7451
|
+
this.#debugLog(`self-announce pause expired after ${Math.round(SELF_ANNOUNCE_PAUSE_MAX_MS / 1000)}s — ` +
|
|
7452
|
+
`releasing it; whoever took it never gave it back`);
|
|
7453
|
+
}
|
|
7354
7454
|
};
|
|
7455
|
+
const expiry = setTimeout(() => release(true), SELF_ANNOUNCE_PAUSE_MAX_MS);
|
|
7456
|
+
expiry.unref?.();
|
|
7457
|
+
return () => release(false);
|
|
7355
7458
|
}
|
|
7356
7459
|
async #runSelfAnnounce(force, deadlineMs) {
|
|
7357
7460
|
if (this.#selfAnnouncePromise) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.157",
|
|
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",
|