mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
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:
1 parent
57902e5cb5
commit
70aab38470
2 files changed
+96
-3
No files matched your search
+39
-3
@@ -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() {
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user