@decentnetwork/peer 0.1.154 → 0.1.156
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 +156 -14
- package/package.json +1 -1
package/dist/peer.js
CHANGED
|
@@ -89,6 +89,21 @@ const NODE_BLACKLIST_MAX_TTL_MS = readEnvInt("DECENT_NODE_BLACKLIST_MAX_TTL_MS",
|
|
|
89
89
|
const FRIEND_ANNOUNCE_ATTEMPTS = readEnvInt("DECENT_FRIEND_ANNOUNCE_ATTEMPTS", 1);
|
|
90
90
|
const JOIN_ANNOUNCE_TIMEOUT_MS = readEnvInt("DECENT_JOIN_ANNOUNCE_TIMEOUT_MS", 12000);
|
|
91
91
|
const SELF_ANNOUNCE_INTERVAL_MS = readEnvInt("DECENT_SELF_ANNOUNCE_INTERVAL_MS", 20000);
|
|
92
|
+
// Slack on top of a run's own deadline before the watchdog abandons it. The
|
|
93
|
+
// abandoned run is left to finish or hang on its own; what matters is that the
|
|
94
|
+
// NEXT tick is allowed to start.
|
|
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";
|
|
102
|
+
// How long a stored announce entry stays useful to someone looking us up.
|
|
103
|
+
// toxcore expires announce entries on roughly this timescale, so a node that
|
|
104
|
+
// acknowledged a store longer ago than this can no longer be counted on.
|
|
105
|
+
const ANNOUNCE_ENTRY_TTL_MS = readEnvInt("DECENT_ANNOUNCE_ENTRY_TTL_MS", 300_000);
|
|
106
|
+
const FAULT_ANNOUNCE_HANG_RUN = readEnvInt("DECENT_FAULT_ANNOUNCE_HANG", 0);
|
|
92
107
|
// Classic-DHT maintenance cadence. Every tick we get_nodes toward our own key
|
|
93
108
|
// (so neighbours store our address and native peers can find our UDP endpoint)
|
|
94
109
|
// and toward each not-yet-UDP friend's key (so we find theirs and punch).
|
|
@@ -475,6 +490,11 @@ export class Peer {
|
|
|
475
490
|
// 25s forever for stale persisted entries.
|
|
476
491
|
#dhtPkConsecutiveFailures = new Map();
|
|
477
492
|
#lastSelfAnnounceStoredCount = -1;
|
|
493
|
+
// nodeId -> when that node last acknowledged STORING our announce. What
|
|
494
|
+
// callers actually want to know is "can anyone look me up right now", and
|
|
495
|
+
// that is a property of live storage across rounds, not of the round in
|
|
496
|
+
// flight. Counted against ANNOUNCE_ENTRY_TTL_MS on read.
|
|
497
|
+
#selfAnnounceStoredAt = new Map();
|
|
478
498
|
// Diagnostics for the TCP-relay onion path (announce/discovery over TCP).
|
|
479
499
|
// Surfaced in dhtHealth so `agentnet diag` shows whether it's active without
|
|
480
500
|
// needing verbose logs: sent = onion requests handed to relays, recv = onion
|
|
@@ -536,6 +556,13 @@ export class Peer {
|
|
|
536
556
|
#profileRetryTimers = new Map();
|
|
537
557
|
#greetingSentTo = new Set();
|
|
538
558
|
#selfAnnouncePromise;
|
|
559
|
+
#selfAnnounceRunCount = 0;
|
|
560
|
+
// Bumped when the watchdog abandons a run. The abandoned run is still alive
|
|
561
|
+
// and would otherwise keep writing #announceRouteUsed underneath the round
|
|
562
|
+
// that replaced it — and step2 rejects a ping_id whose route changed, so
|
|
563
|
+
// that interference would cost stores. It cannot be killed, but it can be
|
|
564
|
+
// told to stop at its next wave boundary.
|
|
565
|
+
#selfAnnounceEpoch = 0;
|
|
539
566
|
#selfAnnouncePauseDepth = 0;
|
|
540
567
|
#started = false;
|
|
541
568
|
constructor(opts) {
|
|
@@ -1080,6 +1107,13 @@ export class Peer {
|
|
|
1080
1107
|
}
|
|
1081
1108
|
const resumeSelfAnnounce = this.#pauseSelfAnnounce();
|
|
1082
1109
|
try {
|
|
1110
|
+
// Fault injection, off unless DECENT_FAULT_REQUEST_HANG=1. Hangs inside
|
|
1111
|
+
// the paused region — the shape that killed self-announce in the
|
|
1112
|
+
// browser — so the pause expiry can be tested rather than argued for.
|
|
1113
|
+
if (FAULT_REQUEST_HANG) {
|
|
1114
|
+
this.#debugLog("FAULT: hanging inside the self-announce pause forever");
|
|
1115
|
+
await new Promise(() => { });
|
|
1116
|
+
}
|
|
1083
1117
|
if (this.#selfAnnouncePromise) {
|
|
1084
1118
|
await this.#selfAnnouncePromise.catch(() => { });
|
|
1085
1119
|
}
|
|
@@ -3633,6 +3667,17 @@ export class Peer {
|
|
|
3633
3667
|
return [];
|
|
3634
3668
|
}
|
|
3635
3669
|
this.#lastSelfAnnounceMs = now;
|
|
3670
|
+
// Fault injection, off unless DECENT_FAULT_ANNOUNCE_HANG names a run
|
|
3671
|
+
// index. Reproduces the browser wedge — a run that never settles — so the
|
|
3672
|
+
// watchdog in #runSelfAnnounce can be tested for real instead of only
|
|
3673
|
+
// reasoned about. Same spirit as DECENT_ANNOUNCE_DEBUG: costs one integer
|
|
3674
|
+
// compare when unset.
|
|
3675
|
+
const myEpoch = this.#selfAnnounceEpoch;
|
|
3676
|
+
this.#selfAnnounceRunCount += 1;
|
|
3677
|
+
if (FAULT_ANNOUNCE_HANG_RUN > 0 && this.#selfAnnounceRunCount === FAULT_ANNOUNCE_HANG_RUN) {
|
|
3678
|
+
this.#debugLog(`FAULT: hanging self-announce run #${this.#selfAnnounceRunCount} forever`);
|
|
3679
|
+
await new Promise(() => { });
|
|
3680
|
+
}
|
|
3636
3681
|
const storedNodes = [];
|
|
3637
3682
|
// An onion announce only STORES on the nodes whose key is closest to OURS,
|
|
3638
3683
|
// and toxcore reaches them by following the "here are closer nodes" lists
|
|
@@ -3671,15 +3716,27 @@ export class Peer {
|
|
|
3671
3716
|
queue.sort((x, y) => xorCloser(selfPk, x.nodePk, y.nodePk));
|
|
3672
3717
|
};
|
|
3673
3718
|
enqueueNodes(this.#knownNodes.length > 0 ? this.#knownNodes : this.#opts.bootstrapNodes);
|
|
3674
|
-
//
|
|
3675
|
-
//
|
|
3676
|
-
|
|
3719
|
+
// No reset here. Zeroing at the START of every round is why dhtHealth
|
|
3720
|
+
// read 0 for the first seconds of each round, and why a round cut short by
|
|
3721
|
+
// the deadline published 0 even though ten nodes were still holding our
|
|
3722
|
+
// announce from 10s earlier. Measured before this change: a healthy peer
|
|
3723
|
+
// reported 0 / 10 / 0 / 0 / 10 across a 135s poll. Anyone sampling at the
|
|
3724
|
+
// wrong moment — a user reading the UI, or me reading it and reporting it
|
|
3725
|
+
// as a finding — concluded the peer was unfindable when it was not.
|
|
3726
|
+
// "Never ran" is still distinguishable: #lastSelfAnnounceStoredCount
|
|
3727
|
+
// stays -1 until the first round records something.
|
|
3728
|
+
if (this.#lastSelfAnnounceStoredCount < 0)
|
|
3729
|
+
this.#lastSelfAnnounceStoredCount = 0;
|
|
3677
3730
|
const STORE_TARGET = 4;
|
|
3678
3731
|
let waves = 0;
|
|
3679
3732
|
while (queue.length > 0 && storedNodes.length < STORE_TARGET && waves < 16) {
|
|
3680
3733
|
if (Date.now() >= deadlineMs) {
|
|
3681
3734
|
this.#debugLog("self announce stopped at deadline");
|
|
3682
|
-
this.#
|
|
3735
|
+
this.#publishSelfAnnounceStoredCount();
|
|
3736
|
+
return storedNodes;
|
|
3737
|
+
}
|
|
3738
|
+
if (this.#selfAnnounceEpoch !== myEpoch) {
|
|
3739
|
+
this.#debugLog("self announce abandoned by watchdog — stopping at wave boundary");
|
|
3683
3740
|
return storedNodes;
|
|
3684
3741
|
}
|
|
3685
3742
|
waves += 1;
|
|
@@ -3784,9 +3841,17 @@ export class Peer {
|
|
|
3784
3841
|
if (final.isStored === 2) {
|
|
3785
3842
|
storedNodes.push(c.node);
|
|
3786
3843
|
this.#debugLog(`self announce STORED on ${c.node.host}:${c.node.port} (total ${storedNodes.length})`);
|
|
3787
|
-
//
|
|
3788
|
-
//
|
|
3789
|
-
|
|
3844
|
+
// Publish the running count only once it EXCEEDS the last completed
|
|
3845
|
+
// round's. Each round starts from an empty storedNodes, so writing it
|
|
3846
|
+
// through unconditionally made dhtHealth read 0 for the first second
|
|
3847
|
+
// or so of every round — and anyone who polled in that window saw
|
|
3848
|
+
// "stored on 0 nodes" and concluded the peer was unfindable. That
|
|
3849
|
+
// reading is what made the Air look permanently broken, and I
|
|
3850
|
+
// reported it as a finding once before checking whether it settled.
|
|
3851
|
+
// Measured on this Mac: the same peer alternates 0 / 9 / 10 / 0
|
|
3852
|
+
// across a two-minute poll while every round stores on ten nodes.
|
|
3853
|
+
this.#selfAnnounceStoredAt.set(`${c.node.host}:${c.node.port}`, Date.now());
|
|
3854
|
+
this.#publishSelfAnnounceStoredCount();
|
|
3790
3855
|
}
|
|
3791
3856
|
const discovered = parsePackedNodes(final.nodes);
|
|
3792
3857
|
if (discovered.length > 0) {
|
|
@@ -3797,12 +3862,22 @@ export class Peer {
|
|
|
3797
3862
|
}
|
|
3798
3863
|
}
|
|
3799
3864
|
}
|
|
3800
|
-
// Track
|
|
3801
|
-
//
|
|
3802
|
-
//
|
|
3803
|
-
|
|
3865
|
+
// Track live acknowledged storage so dhtHealth() can still surface "DHT
|
|
3866
|
+
// discovery layer alive but our announce isn't landing anywhere" — that
|
|
3867
|
+
// now shows up as the count DECAYING to 0 as entries age out, instead of
|
|
3868
|
+
// flickering to 0 once per round.
|
|
3869
|
+
this.#publishSelfAnnounceStoredCount();
|
|
3804
3870
|
return storedNodes;
|
|
3805
3871
|
}
|
|
3872
|
+
/** Count nodes whose acknowledged store is still within its TTL. */
|
|
3873
|
+
#publishSelfAnnounceStoredCount() {
|
|
3874
|
+
const cutoff = Date.now() - ANNOUNCE_ENTRY_TTL_MS;
|
|
3875
|
+
for (const [id, at] of this.#selfAnnounceStoredAt) {
|
|
3876
|
+
if (at < cutoff)
|
|
3877
|
+
this.#selfAnnounceStoredAt.delete(id);
|
|
3878
|
+
}
|
|
3879
|
+
this.#lastSelfAnnounceStoredCount = this.#selfAnnounceStoredAt.size;
|
|
3880
|
+
}
|
|
3806
3881
|
#ensureSelfAnnounceLoop() {
|
|
3807
3882
|
if (this.#selfAnnounceTimer || SELF_ANNOUNCE_INTERVAL_MS <= 0) {
|
|
3808
3883
|
return;
|
|
@@ -7285,22 +7360,89 @@ export class Peer {
|
|
|
7285
7360
|
const recentBonus = Date.now() - h.lastOkMs < 60_000 ? 2 : 0;
|
|
7286
7361
|
return (h.ok * 2) - h.fail + recentBonus;
|
|
7287
7362
|
}
|
|
7363
|
+
/**
|
|
7364
|
+
* Pause the self-announce loop, with an expiry.
|
|
7365
|
+
*
|
|
7366
|
+
* The pause is released in a `finally`, which is correct right up until the
|
|
7367
|
+
* guarded body never finishes. sendFriendRequest awaits a relay send inside
|
|
7368
|
+
* that body, and in a browser a backgrounded tab can leave that socket
|
|
7369
|
+
* neither open nor errored — so the `finally` never runs, the depth stays at
|
|
7370
|
+
* 1, and self-announce is dead forever.
|
|
7371
|
+
*
|
|
7372
|
+
* This latch sits BEFORE the watchdog in #ensureSelfAnnounceLoop: the tick
|
|
7373
|
+
* returns on pause depth without ever calling #runSelfAnnounce, so bounding
|
|
7374
|
+
* the run was not enough on its own. Measured on app.beagle.chat after the
|
|
7375
|
+
* watchdog shipped: announce age still climbing past 700s while the page's
|
|
7376
|
+
* other timers ran normally.
|
|
7377
|
+
*
|
|
7378
|
+
* A pause is a short-lived thing — a few seconds around one send. One that
|
|
7379
|
+
* outlives this window is a bug somewhere upstream, and the loop matters
|
|
7380
|
+
* more than the pause does.
|
|
7381
|
+
*/
|
|
7288
7382
|
#pauseSelfAnnounce() {
|
|
7289
7383
|
this.#selfAnnouncePauseDepth += 1;
|
|
7290
|
-
|
|
7384
|
+
let released = false;
|
|
7385
|
+
const release = (viaTimeout) => {
|
|
7386
|
+
if (released)
|
|
7387
|
+
return;
|
|
7388
|
+
released = true;
|
|
7389
|
+
clearTimeout(expiry);
|
|
7291
7390
|
this.#selfAnnouncePauseDepth = Math.max(0, this.#selfAnnouncePauseDepth - 1);
|
|
7391
|
+
if (viaTimeout) {
|
|
7392
|
+
this.#debugLog(`self-announce pause expired after ${Math.round(SELF_ANNOUNCE_PAUSE_MAX_MS / 1000)}s — ` +
|
|
7393
|
+
`releasing it; whoever took it never gave it back`);
|
|
7394
|
+
}
|
|
7292
7395
|
};
|
|
7396
|
+
const expiry = setTimeout(() => release(true), SELF_ANNOUNCE_PAUSE_MAX_MS);
|
|
7397
|
+
expiry.unref?.();
|
|
7398
|
+
return () => release(false);
|
|
7293
7399
|
}
|
|
7294
7400
|
async #runSelfAnnounce(force, deadlineMs) {
|
|
7295
7401
|
if (this.#selfAnnouncePromise) {
|
|
7296
7402
|
await this.#selfAnnouncePromise.catch(() => { });
|
|
7297
7403
|
}
|
|
7298
|
-
|
|
7404
|
+
// Hard deadline around the WHOLE run, not just the per-node waiter.
|
|
7405
|
+
//
|
|
7406
|
+
// #ensureSelfAnnounceLoop skips a tick while #selfAnnouncePromise is set,
|
|
7407
|
+
// so one run that never settles kills self-announce FOREVER — silently,
|
|
7408
|
+
// and with dhtHealth still reporting the last good numbers. Measured on
|
|
7409
|
+
// app.beagle.chat 2026-08-31: lastSelfAnnounceMs frozen 1731s, stored
|
|
7410
|
+
// stuck at 11, nobody able to look us up. lastSelfAnnounceMs is stamped
|
|
7411
|
+
// on ENTRY, so a frozen value proves the function was never re-entered —
|
|
7412
|
+
// the loop was latched, not failing.
|
|
7413
|
+
//
|
|
7414
|
+
// The hang is in the transport: #sendAnnounceAndWait awaits
|
|
7415
|
+
// #sendThroughOnionPath BEFORE it awaits the timeout-guarded waiter, and
|
|
7416
|
+
// in a browser that send goes over a relay WebSocket. A backgrounded tab
|
|
7417
|
+
// can leave that socket neither open nor errored, so the await never
|
|
7418
|
+
// settles and Promise.allSettled underneath waits forever. Native peers
|
|
7419
|
+
// send over UDP and never hit it, which is why this was browser-only.
|
|
7420
|
+
//
|
|
7421
|
+
// Timing out the specific await would fix today's hang; this fixes the
|
|
7422
|
+
// class. A self-healing loop must not depend on every transport path
|
|
7423
|
+
// being timeout-correct to stay alive.
|
|
7424
|
+
const budgetMs = Math.max(0, deadlineMs - Date.now()) + SELF_ANNOUNCE_WATCHDOG_MARGIN_MS;
|
|
7425
|
+
let watchdog;
|
|
7426
|
+
const guarded = Promise.race([
|
|
7427
|
+
this.#announceSelfBestEffort(force, deadlineMs),
|
|
7428
|
+
new Promise((resolve) => {
|
|
7429
|
+
watchdog = setTimeout(() => {
|
|
7430
|
+
this.#debugLog(`self announce watchdog fired after ${Math.round(budgetMs / 1000)}s — ` +
|
|
7431
|
+
`abandoning the run so the loop can retry`);
|
|
7432
|
+
this.#selfAnnounceEpoch += 1;
|
|
7433
|
+
resolve([]);
|
|
7434
|
+
}, budgetMs);
|
|
7435
|
+
watchdog.unref?.();
|
|
7436
|
+
}),
|
|
7437
|
+
]);
|
|
7438
|
+
this.#selfAnnouncePromise = guarded;
|
|
7299
7439
|
try {
|
|
7300
|
-
const result = await
|
|
7440
|
+
const result = await guarded;
|
|
7301
7441
|
return result ?? [];
|
|
7302
7442
|
}
|
|
7303
7443
|
finally {
|
|
7444
|
+
if (watchdog)
|
|
7445
|
+
clearTimeout(watchdog);
|
|
7304
7446
|
this.#selfAnnouncePromise = undefined;
|
|
7305
7447
|
}
|
|
7306
7448
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.156",
|
|
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",
|