mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
On cold login the app opened WebSockets to a user's full NIP-65 read set
(in prod often ~20 relays, most dead). The flood of failing connections
churned the main thread and starved the nip-07 extension's postMessage
round-trips, so encrypted lists (blocks, subscriptions, hashtags) were
fetched but timed out during decryption ("message channel closed",
"Decryption timed out").
Bound every read/subscribe path to MAX_SUBSCRIBE_RELAYS=8 via a shared,
test-aware capReadRelays helper, applied at the chokepoints:
- client.js applyRelayPreferences (this.relays / this.readRelays)
- relayBatchFetcher.js fetchListIncrementally (blocks, subs, hashtags)
Writes stay uncapped (publish reach preserved).
capReadRelays is user-relays-first (a user's own relays are where their
data authoritatively lives, and small fast-path limits would otherwise
crowd them out — the regression that reverted the first attempt) while
reserving RESERVED_DEFAULT_RELAY_SLOTS=2 for reliable default aggregators
so the "~20 personal relays, all dead" case still reaches a live relay.
Under test the reserved core resolves to the test-relay override, keeping
harnesses isolated.
Tests: new relay-subscribe-cap coverage (bounded + reserved-defaults +
writes-uncapped); spec-corrected user-blocks, subscriptions-manager and
nostr/client fetch tests to bounded (not exact) relay fan-out while
keeping all behavioral/security assertions (per-relay coverage, correct
decrypt, block hydration, dedup) intact. See TEST_INTEGRITY.md and
KNOWN_BUGS #0/#4. Full unit + dm:unit suites green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
67 lines
2.6 KiB
JavaScript
67 lines
2.6 KiB
JavaScript
// 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, RESERVED_DEFAULT_RELAY_SLOTS } = await import(
|
|
"../js/nostr/toolkit.js"
|
|
);
|
|
|
|
test("subscribe/read relays are capped and ALWAYS include defaults; writes are not", () => {
|
|
// A user's NIP-65 list of dead relays that contains NONE of the bundled
|
|
// defaults — the real-env failure mode that starved decryption.
|
|
const deadList = [
|
|
"wss://dead1.example",
|
|
"wss://dead2.example",
|
|
"wss://dead3.example",
|
|
"wss://dead4.example",
|
|
"wss://dead5.example",
|
|
"wss://dead6.example",
|
|
"wss://dead7.example",
|
|
"wss://dead8.example",
|
|
"wss://dead9.example",
|
|
"wss://dead10.example",
|
|
];
|
|
|
|
nostrClient.applyRelayPreferences({ all: deadList, write: deadList });
|
|
|
|
// Read/subscribe set is bounded.
|
|
assert.ok(
|
|
nostrClient.relays.length <= 8,
|
|
`read set should be capped (<=8), got ${nostrClient.relays.length}`,
|
|
);
|
|
|
|
// Reliable defaults must be present EVEN THOUGH none were in the user's list —
|
|
// otherwise reads point only at the user's dead relays and decryption starves.
|
|
// The cap reserves a small guaranteed core for defaults (the rest of the set
|
|
// stays the user's own relays, which is where their data authoritatively
|
|
// lives), so at least RESERVED_DEFAULT_RELAY_SLOTS defaults must survive.
|
|
const survivingDefaults = DEFAULT_RELAY_URLS.filter((url) =>
|
|
nostrClient.relays.includes(url),
|
|
);
|
|
assert.ok(
|
|
survivingDefaults.length >= RESERVED_DEFAULT_RELAY_SLOTS,
|
|
`read set must keep >=${RESERVED_DEFAULT_RELAY_SLOTS} reliable defaults, got ${survivingDefaults.length}`,
|
|
);
|
|
|
|
// Writes are NOT capped and must NOT be forced to include defaults.
|
|
assert.equal(
|
|
nostrClient.writeRelays.length,
|
|
new Set(deadList).size,
|
|
"write set must be the full configured set (publish reach preserved)",
|
|
);
|
|
assert.ok(
|
|
!nostrClient.writeRelays.includes(DEFAULT_RELAY_URLS[0]),
|
|
"defaults must not be injected into the write set",
|
|
);
|
|
});
|