perf(relay): cap the subscribe/read relay set (interim RelayHealth)

A user's NIP-65 list can carry ~20 relays, most dead. Subscribing on all of
them is the dominant REQ-storm amplifier: every subsystem fans every query
across all relays and re-fires on each flaky-relay reconnect. Cap the
read/subscribe set to a bounded core (6), prioritizing the bundled known-good
defaults; WRITES still fan out to the full set so publish reach is unaffected.
Interim until RelayHealth ranks by liveness (docs/architecture-refactor.md P1).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-14 10:03:00 -04:00
1 parent 57902e5cb5
commit 70aab38470
2 files changed
+96 -3

No files matched your search

+39 -3
View File
@@ -228,6 +228,29 @@ import {
import { inferMimeTypeFromUrl } from "../utils/mime.js";
import { VideoEventBuffer } from "./videoEventBuffer.js";
// Max relays the client will SUBSCRIBE/read from at once. Bounds the REQ
// fan-out regardless of how large a user's NIP-65 relay list is. Writes are not
// capped (see applyRelayPreferences). Interim until RelayHealth ranks relays by
// liveness — see docs/architecture-refactor.md.
const MAX_SUBSCRIBE_RELAYS = 6;
// Return at most MAX_SUBSCRIBE_RELAYS urls, prioritizing the bundled known-good
// defaults (which are large, reliable aggregators) ahead of arbitrary NIP-65
// entries, then filling with the remaining urls in their original order.
function capSubscribeRelays(urls) {
const list = Array.isArray(urls) ? urls.filter((u) => typeof u === "string" && u) : [];
if (list.length <= MAX_SUBSCRIBE_RELAYS) {
return Array.from(new Set(list));
}
const defaults = new Set(DEFAULT_RELAY_URLS);
const prioritized = [];
const rest = [];
for (const url of list) {
(defaults.has(url) ? prioritized : rest).push(url);
}
return Array.from(new Set([...prioritized, ...rest])).slice(0, MAX_SUBSCRIBE_RELAYS);
}
function normalizeProfileFromEvent(event) {
if (!event || !event.content) return null;
try {
@@ -861,9 +884,22 @@ export class NostrClient {
: effectiveAll
);
this.relays = effectiveAll.length ? effectiveAll : Array.from(RELAY_URLS);
this.readRelays = sanitizedRead.length ? sanitizedRead : Array.from(this.relays);
this.writeRelays = sanitizedWrite.length ? sanitizedWrite : Array.from(this.relays);
// Cap the SUBSCRIPTION/read set. A user's NIP-65 list can carry ~20 relays,
// most dead; subscribing on all of them is the dominant REQ-storm amplifier
// (every subsystem fans every query across all relays and re-fires on each
// flaky-relay reconnect). Reads go to a bounded core (known-good defaults
// prioritized); WRITES still fan out to the full set so publish reach is
// unaffected. This is the interim trim until RelayHealth ranks by liveness
// (see docs/architecture-refactor.md, P1).
const cappedRead = capSubscribeRelays(effectiveAll);
this.relays = cappedRead.length ? cappedRead : Array.from(RELAY_URLS);
this.readRelays = sanitizedRead.length
? capSubscribeRelays(sanitizedRead)
: Array.from(this.relays);
this.writeRelays = sanitizedWrite.length
? sanitizedWrite
: Array.from(effectiveAll.length ? effectiveAll : RELAY_URLS);
}
getWatchHistoryCacheTtlMs() {
+57
View File
@@ -0,0 +1,57 @@
// Scenario (SCN-relay-subscribe-cap):
// Given a user's NIP-65 relay list with many relays (the real-env case had
// ~20, most dead),
// When the client applies those relay preferences,
// Then the SUBSCRIBE/read set is capped to a small bounded core (so query
// fan-out can't explode and re-storm on reconnect),
// And the WRITE set is NOT capped (publish reach is preserved),
// And the bundled known-good defaults are prioritized into the read set.
import "./test-helpers/setup-localstorage.mjs";
import { test } from "node:test";
import assert from "node:assert/strict";
const { nostrClient } = await import("../js/nostrClientFacade.js");
const { DEFAULT_RELAY_URLS } = await import("../js/nostr/toolkit.js");
test("subscribe/read relays are capped; write relays are not", () => {
const many = [
"wss://dead1.example",
"wss://dead2.example",
"wss://dead3.example",
"wss://dead4.example",
"wss://dead5.example",
"wss://dead6.example",
"wss://dead7.example",
"wss://dead8.example",
...DEFAULT_RELAY_URLS, // known-good, listed last on purpose
];
nostrClient.applyRelayPreferences({ all: many, write: many });
// Read/subscribe set is bounded.
assert.ok(
nostrClient.relays.length <= 6,
`read set should be capped (<=6), got ${nostrClient.relays.length}`,
);
assert.ok(
nostrClient.readRelays.length <= 6,
`readRelays should be capped (<=6), got ${nostrClient.readRelays.length}`,
);
// Known-good defaults are prioritized into the capped read set despite being
// listed last in the input.
for (const url of DEFAULT_RELAY_URLS) {
assert.ok(
nostrClient.relays.includes(url),
`known-good default ${url} should survive the cap`,
);
}
// Writes are NOT capped — publish reach preserved.
assert.equal(
nostrClient.writeRelays.length,
new Set(many).size,
"write set must include every configured relay",
);
});