@decentnetwork/peer 0.1.154 → 0.1.155
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 +111 -13
- package/package.json +1 -1
package/dist/peer.js
CHANGED
|
@@ -89,6 +89,15 @@ 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
|
+
// How long a stored announce entry stays useful to someone looking us up.
|
|
97
|
+
// toxcore expires announce entries on roughly this timescale, so a node that
|
|
98
|
+
// acknowledged a store longer ago than this can no longer be counted on.
|
|
99
|
+
const ANNOUNCE_ENTRY_TTL_MS = readEnvInt("DECENT_ANNOUNCE_ENTRY_TTL_MS", 300_000);
|
|
100
|
+
const FAULT_ANNOUNCE_HANG_RUN = readEnvInt("DECENT_FAULT_ANNOUNCE_HANG", 0);
|
|
92
101
|
// Classic-DHT maintenance cadence. Every tick we get_nodes toward our own key
|
|
93
102
|
// (so neighbours store our address and native peers can find our UDP endpoint)
|
|
94
103
|
// and toward each not-yet-UDP friend's key (so we find theirs and punch).
|
|
@@ -475,6 +484,11 @@ export class Peer {
|
|
|
475
484
|
// 25s forever for stale persisted entries.
|
|
476
485
|
#dhtPkConsecutiveFailures = new Map();
|
|
477
486
|
#lastSelfAnnounceStoredCount = -1;
|
|
487
|
+
// nodeId -> when that node last acknowledged STORING our announce. What
|
|
488
|
+
// callers actually want to know is "can anyone look me up right now", and
|
|
489
|
+
// that is a property of live storage across rounds, not of the round in
|
|
490
|
+
// flight. Counted against ANNOUNCE_ENTRY_TTL_MS on read.
|
|
491
|
+
#selfAnnounceStoredAt = new Map();
|
|
478
492
|
// Diagnostics for the TCP-relay onion path (announce/discovery over TCP).
|
|
479
493
|
// Surfaced in dhtHealth so `agentnet diag` shows whether it's active without
|
|
480
494
|
// needing verbose logs: sent = onion requests handed to relays, recv = onion
|
|
@@ -536,6 +550,13 @@ export class Peer {
|
|
|
536
550
|
#profileRetryTimers = new Map();
|
|
537
551
|
#greetingSentTo = new Set();
|
|
538
552
|
#selfAnnouncePromise;
|
|
553
|
+
#selfAnnounceRunCount = 0;
|
|
554
|
+
// Bumped when the watchdog abandons a run. The abandoned run is still alive
|
|
555
|
+
// and would otherwise keep writing #announceRouteUsed underneath the round
|
|
556
|
+
// that replaced it — and step2 rejects a ping_id whose route changed, so
|
|
557
|
+
// that interference would cost stores. It cannot be killed, but it can be
|
|
558
|
+
// told to stop at its next wave boundary.
|
|
559
|
+
#selfAnnounceEpoch = 0;
|
|
539
560
|
#selfAnnouncePauseDepth = 0;
|
|
540
561
|
#started = false;
|
|
541
562
|
constructor(opts) {
|
|
@@ -3633,6 +3654,17 @@ export class Peer {
|
|
|
3633
3654
|
return [];
|
|
3634
3655
|
}
|
|
3635
3656
|
this.#lastSelfAnnounceMs = now;
|
|
3657
|
+
// Fault injection, off unless DECENT_FAULT_ANNOUNCE_HANG names a run
|
|
3658
|
+
// index. Reproduces the browser wedge — a run that never settles — so the
|
|
3659
|
+
// watchdog in #runSelfAnnounce can be tested for real instead of only
|
|
3660
|
+
// reasoned about. Same spirit as DECENT_ANNOUNCE_DEBUG: costs one integer
|
|
3661
|
+
// compare when unset.
|
|
3662
|
+
const myEpoch = this.#selfAnnounceEpoch;
|
|
3663
|
+
this.#selfAnnounceRunCount += 1;
|
|
3664
|
+
if (FAULT_ANNOUNCE_HANG_RUN > 0 && this.#selfAnnounceRunCount === FAULT_ANNOUNCE_HANG_RUN) {
|
|
3665
|
+
this.#debugLog(`FAULT: hanging self-announce run #${this.#selfAnnounceRunCount} forever`);
|
|
3666
|
+
await new Promise(() => { });
|
|
3667
|
+
}
|
|
3636
3668
|
const storedNodes = [];
|
|
3637
3669
|
// An onion announce only STORES on the nodes whose key is closest to OURS,
|
|
3638
3670
|
// and toxcore reaches them by following the "here are closer nodes" lists
|
|
@@ -3671,15 +3703,27 @@ export class Peer {
|
|
|
3671
3703
|
queue.sort((x, y) => xorCloser(selfPk, x.nodePk, y.nodePk));
|
|
3672
3704
|
};
|
|
3673
3705
|
enqueueNodes(this.#knownNodes.length > 0 ? this.#knownNodes : this.#opts.bootstrapNodes);
|
|
3674
|
-
//
|
|
3675
|
-
//
|
|
3676
|
-
|
|
3706
|
+
// No reset here. Zeroing at the START of every round is why dhtHealth
|
|
3707
|
+
// read 0 for the first seconds of each round, and why a round cut short by
|
|
3708
|
+
// the deadline published 0 even though ten nodes were still holding our
|
|
3709
|
+
// announce from 10s earlier. Measured before this change: a healthy peer
|
|
3710
|
+
// reported 0 / 10 / 0 / 0 / 10 across a 135s poll. Anyone sampling at the
|
|
3711
|
+
// wrong moment — a user reading the UI, or me reading it and reporting it
|
|
3712
|
+
// as a finding — concluded the peer was unfindable when it was not.
|
|
3713
|
+
// "Never ran" is still distinguishable: #lastSelfAnnounceStoredCount
|
|
3714
|
+
// stays -1 until the first round records something.
|
|
3715
|
+
if (this.#lastSelfAnnounceStoredCount < 0)
|
|
3716
|
+
this.#lastSelfAnnounceStoredCount = 0;
|
|
3677
3717
|
const STORE_TARGET = 4;
|
|
3678
3718
|
let waves = 0;
|
|
3679
3719
|
while (queue.length > 0 && storedNodes.length < STORE_TARGET && waves < 16) {
|
|
3680
3720
|
if (Date.now() >= deadlineMs) {
|
|
3681
3721
|
this.#debugLog("self announce stopped at deadline");
|
|
3682
|
-
this.#
|
|
3722
|
+
this.#publishSelfAnnounceStoredCount();
|
|
3723
|
+
return storedNodes;
|
|
3724
|
+
}
|
|
3725
|
+
if (this.#selfAnnounceEpoch !== myEpoch) {
|
|
3726
|
+
this.#debugLog("self announce abandoned by watchdog — stopping at wave boundary");
|
|
3683
3727
|
return storedNodes;
|
|
3684
3728
|
}
|
|
3685
3729
|
waves += 1;
|
|
@@ -3784,9 +3828,17 @@ export class Peer {
|
|
|
3784
3828
|
if (final.isStored === 2) {
|
|
3785
3829
|
storedNodes.push(c.node);
|
|
3786
3830
|
this.#debugLog(`self announce STORED on ${c.node.host}:${c.node.port} (total ${storedNodes.length})`);
|
|
3787
|
-
//
|
|
3788
|
-
//
|
|
3789
|
-
|
|
3831
|
+
// Publish the running count only once it EXCEEDS the last completed
|
|
3832
|
+
// round's. Each round starts from an empty storedNodes, so writing it
|
|
3833
|
+
// through unconditionally made dhtHealth read 0 for the first second
|
|
3834
|
+
// or so of every round — and anyone who polled in that window saw
|
|
3835
|
+
// "stored on 0 nodes" and concluded the peer was unfindable. That
|
|
3836
|
+
// reading is what made the Air look permanently broken, and I
|
|
3837
|
+
// reported it as a finding once before checking whether it settled.
|
|
3838
|
+
// Measured on this Mac: the same peer alternates 0 / 9 / 10 / 0
|
|
3839
|
+
// across a two-minute poll while every round stores on ten nodes.
|
|
3840
|
+
this.#selfAnnounceStoredAt.set(`${c.node.host}:${c.node.port}`, Date.now());
|
|
3841
|
+
this.#publishSelfAnnounceStoredCount();
|
|
3790
3842
|
}
|
|
3791
3843
|
const discovered = parsePackedNodes(final.nodes);
|
|
3792
3844
|
if (discovered.length > 0) {
|
|
@@ -3797,12 +3849,22 @@ export class Peer {
|
|
|
3797
3849
|
}
|
|
3798
3850
|
}
|
|
3799
3851
|
}
|
|
3800
|
-
// Track
|
|
3801
|
-
//
|
|
3802
|
-
//
|
|
3803
|
-
|
|
3852
|
+
// Track live acknowledged storage so dhtHealth() can still surface "DHT
|
|
3853
|
+
// discovery layer alive but our announce isn't landing anywhere" — that
|
|
3854
|
+
// now shows up as the count DECAYING to 0 as entries age out, instead of
|
|
3855
|
+
// flickering to 0 once per round.
|
|
3856
|
+
this.#publishSelfAnnounceStoredCount();
|
|
3804
3857
|
return storedNodes;
|
|
3805
3858
|
}
|
|
3859
|
+
/** Count nodes whose acknowledged store is still within its TTL. */
|
|
3860
|
+
#publishSelfAnnounceStoredCount() {
|
|
3861
|
+
const cutoff = Date.now() - ANNOUNCE_ENTRY_TTL_MS;
|
|
3862
|
+
for (const [id, at] of this.#selfAnnounceStoredAt) {
|
|
3863
|
+
if (at < cutoff)
|
|
3864
|
+
this.#selfAnnounceStoredAt.delete(id);
|
|
3865
|
+
}
|
|
3866
|
+
this.#lastSelfAnnounceStoredCount = this.#selfAnnounceStoredAt.size;
|
|
3867
|
+
}
|
|
3806
3868
|
#ensureSelfAnnounceLoop() {
|
|
3807
3869
|
if (this.#selfAnnounceTimer || SELF_ANNOUNCE_INTERVAL_MS <= 0) {
|
|
3808
3870
|
return;
|
|
@@ -7295,12 +7357,48 @@ export class Peer {
|
|
|
7295
7357
|
if (this.#selfAnnouncePromise) {
|
|
7296
7358
|
await this.#selfAnnouncePromise.catch(() => { });
|
|
7297
7359
|
}
|
|
7298
|
-
|
|
7360
|
+
// Hard deadline around the WHOLE run, not just the per-node waiter.
|
|
7361
|
+
//
|
|
7362
|
+
// #ensureSelfAnnounceLoop skips a tick while #selfAnnouncePromise is set,
|
|
7363
|
+
// so one run that never settles kills self-announce FOREVER — silently,
|
|
7364
|
+
// and with dhtHealth still reporting the last good numbers. Measured on
|
|
7365
|
+
// app.beagle.chat 2026-08-31: lastSelfAnnounceMs frozen 1731s, stored
|
|
7366
|
+
// stuck at 11, nobody able to look us up. lastSelfAnnounceMs is stamped
|
|
7367
|
+
// on ENTRY, so a frozen value proves the function was never re-entered —
|
|
7368
|
+
// the loop was latched, not failing.
|
|
7369
|
+
//
|
|
7370
|
+
// The hang is in the transport: #sendAnnounceAndWait awaits
|
|
7371
|
+
// #sendThroughOnionPath BEFORE it awaits the timeout-guarded waiter, and
|
|
7372
|
+
// in a browser that send goes over a relay WebSocket. A backgrounded tab
|
|
7373
|
+
// can leave that socket neither open nor errored, so the await never
|
|
7374
|
+
// settles and Promise.allSettled underneath waits forever. Native peers
|
|
7375
|
+
// send over UDP and never hit it, which is why this was browser-only.
|
|
7376
|
+
//
|
|
7377
|
+
// Timing out the specific await would fix today's hang; this fixes the
|
|
7378
|
+
// class. A self-healing loop must not depend on every transport path
|
|
7379
|
+
// being timeout-correct to stay alive.
|
|
7380
|
+
const budgetMs = Math.max(0, deadlineMs - Date.now()) + SELF_ANNOUNCE_WATCHDOG_MARGIN_MS;
|
|
7381
|
+
let watchdog;
|
|
7382
|
+
const guarded = Promise.race([
|
|
7383
|
+
this.#announceSelfBestEffort(force, deadlineMs),
|
|
7384
|
+
new Promise((resolve) => {
|
|
7385
|
+
watchdog = setTimeout(() => {
|
|
7386
|
+
this.#debugLog(`self announce watchdog fired after ${Math.round(budgetMs / 1000)}s — ` +
|
|
7387
|
+
`abandoning the run so the loop can retry`);
|
|
7388
|
+
this.#selfAnnounceEpoch += 1;
|
|
7389
|
+
resolve([]);
|
|
7390
|
+
}, budgetMs);
|
|
7391
|
+
watchdog.unref?.();
|
|
7392
|
+
}),
|
|
7393
|
+
]);
|
|
7394
|
+
this.#selfAnnouncePromise = guarded;
|
|
7299
7395
|
try {
|
|
7300
|
-
const result = await
|
|
7396
|
+
const result = await guarded;
|
|
7301
7397
|
return result ?? [];
|
|
7302
7398
|
}
|
|
7303
7399
|
finally {
|
|
7400
|
+
if (watchdog)
|
|
7401
|
+
clearTimeout(watchdog);
|
|
7304
7402
|
this.#selfAnnouncePromise = undefined;
|
|
7305
7403
|
}
|
|
7306
7404
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.155",
|
|
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",
|