fix(subscriptions): guard the subscription-list refresh loop

The live subscription-list subscription (kind 30000) delivers our OWN list
event back through handleSubscriptionListEvent, which called updateFromRelays
on EVERY event. updateFromRelays re-fetches the list, the relay echoes the
same event through the live sub, and the handler fires again — a
self-sustaining loop that floods the cold-login burst with redundant
kind-30000 REQs (the kind 30000=19 storm in the real-env console,
KNOWN_BUGS #0). Same shape as the contact-list emit→fetch→emit loop fixed
earlier.

Add an emit-on-change guard: skip the refresh when the event is one we've
already acted on (by id — matters during the pre-decrypt login window where
subsEventCreatedAt isn't set yet) or is not newer than what we've applied.
A genuinely newer list event still refreshes exactly once.

Also extend the channel-sim harness to capture per-filter REQ signatures and
kind-30000 REQ timing, which is how this loop was isolated.

Test: tests/subscriptions-manager.test.mjs — echo of the same event id does
not re-refresh; an older event is ignored; a newer event refreshes once.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-16 09:53:52 -04:00
1 parent 1f07538c66
commit 10ea4f6a22
3 files changed
+77

No files matched your search

+10
View File
@@ -209,6 +209,8 @@ async function main() {
dmHelpersUnavailable: 0,
};
const reqBurst = {}; // kind -> count, captured during the login window
const reqSig = {}; // filter signature -> count, to surface redundant queries
const subsReqTimes = []; // ms-since-login of each kind-30000 REQ (redundancy timing)
let loginAt = Date.now();
let measuringReq = true;
@@ -238,6 +240,10 @@ async function main() {
const msg = JSON.parse(p);
for (const f of msg.slice(2)) {
for (const k of f.kinds || ["?"]) reqBurst[k] = (reqBurst[k] || 0) + 1;
const otherKeys = Object.keys(f).filter((x) => x !== "kinds" && x !== "authors").sort().join(",");
const sig = `kinds=${(f.kinds || []).join("|")}${otherKeys ? " " + otherKeys : ""}${f.authors ? " authors=" + f.authors.length : ""}`;
reqSig[sig] = (reqSig[sig] || 0) + 1;
if ((f.kinds || []).includes(30000)) subsReqTimes.push(Date.now() - loginAt);
}
} catch (_) {}
});
@@ -298,6 +304,10 @@ async function main() {
report(`time -> signer-ready : ${signals.signerReadyAt ?? "NEVER"} ms`);
report(`time -> lists-sync done: ${signals.listsSyncCompleteAt ?? "NEVER"} ms`);
report(`login REQ burst (~10s) : ${reqTotal} frames [${reqTop}]`);
const sigTop = Object.entries(reqSig).sort((a, b) => b[1] - a[1]).slice(0, 12);
report("top REQ filters (count → filter):");
for (const [sig, n] of sigTop) report(` ${String(n).padStart(4)} ${sig}`);
report(`kind-30000 REQ times(ms): [${subsReqTimes.join(", ")}]`);
report(`decrypt timeouts logged: ${signals.decryptTimeouts}`);
report(`channel drops injected : ${drops}`);
report(`breaker opened / recov : ${signals.breakerOpened} / ${signals.breakerRecovered}`);