mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Merge pull request #1858 from PR0M3TH3AN/codex/list-fetch-relay-improvements-8377401680345201038
Improve relay selection and fault tolerance for list fetching
This commit is contained in:
7 files changed
+114
-76
No files matched your search
+43
-1
@@ -3580,7 +3580,29 @@ export class NostrClient {
|
||||
? relayUrls
|
||||
: this.relays;
|
||||
|
||||
const normalizedRelays = sanitizeRelayList(this.getHealthyRelays(relaysToUse));
|
||||
const healthyCandidates = this.getHealthyRelays(relaysToUse);
|
||||
const readPreferences = new Set(Array.isArray(this.readRelays) ? this.readRelays : []);
|
||||
|
||||
// Sort relays: prefer user's read relays first
|
||||
const sortedRelays = healthyCandidates.sort((a, b) => {
|
||||
const aPreferred = readPreferences.has(a);
|
||||
const bPreferred = readPreferences.has(b);
|
||||
if (aPreferred && !bPreferred) return -1;
|
||||
if (!aPreferred && bPreferred) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Cap the number of relays to avoid excessive requests
|
||||
const cappedRelays = sortedRelays.slice(0, 8);
|
||||
const normalizedRelays = sanitizeRelayList(cappedRelays);
|
||||
|
||||
if (isDevMode) {
|
||||
devLogger.log("[fetchListIncrementally] Selected relays:", {
|
||||
count: normalizedRelays.length,
|
||||
relays: normalizedRelays,
|
||||
});
|
||||
}
|
||||
|
||||
const results = [];
|
||||
const concurrencyLimit = 8;
|
||||
|
||||
@@ -3706,6 +3728,7 @@ export class NostrClient {
|
||||
}
|
||||
} else {
|
||||
devLogger.warn(`[fetchListIncrementally] Fetch failed for ${relayUrl}`, error);
|
||||
this.markRelayUnreachable(relayUrl);
|
||||
return { error, ok: false };
|
||||
}
|
||||
}
|
||||
@@ -4084,6 +4107,25 @@ export class NostrClient {
|
||||
return results;
|
||||
}
|
||||
|
||||
markRelayUnreachable(url, ttlMs = 60000) {
|
||||
if (!url || typeof url !== "string") {
|
||||
return;
|
||||
}
|
||||
const normalized = url.trim();
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
this.unreachableRelays.add(normalized);
|
||||
if (ttlMs > 0) {
|
||||
setTimeout(() => {
|
||||
this.unreachableRelays.delete(normalized);
|
||||
if (isDevMode) {
|
||||
devLogger.debug(`[nostr] Cleared temporary failure mark for ${normalized}`);
|
||||
}
|
||||
}, ttlMs);
|
||||
}
|
||||
}
|
||||
|
||||
getHealthyRelays(candidates) {
|
||||
const source = Array.isArray(candidates) ? candidates : this.relays;
|
||||
if (!this.unreachableRelays.size) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import { userLogger, devLogger } from "../utils/logger.js";
|
||||
import { normalizeHashtag } from "../utils/hashtagNormalization.js";
|
||||
import { profileCache } from "../state/profileCache.js";
|
||||
import { DEFAULT_RELAY_URLS } from "../nostr/toolkit.js";
|
||||
import { relayManager } from "../relayManager.js";
|
||||
|
||||
const LOG_PREFIX = "[HashtagPreferences]";
|
||||
const HASHTAG_IDENTIFIER = "bitvid:tag-preferences";
|
||||
@@ -486,26 +487,8 @@ class HashtagPreferencesService {
|
||||
return;
|
||||
}
|
||||
|
||||
const relaySet = new Set();
|
||||
const addRelays = (candidate) => {
|
||||
const sanitized = sanitizeRelayList(candidate);
|
||||
for (const relay of sanitized) {
|
||||
relaySet.add(relay);
|
||||
}
|
||||
};
|
||||
|
||||
addRelays(nostrClient.relays);
|
||||
if (!relaySet.size) {
|
||||
addRelays(nostrClient.readRelays);
|
||||
}
|
||||
if (!relaySet.size) {
|
||||
addRelays(nostrClient.writeRelays);
|
||||
}
|
||||
if (!relaySet.size) {
|
||||
addRelays(DEFAULT_RELAY_URLS);
|
||||
}
|
||||
|
||||
const relays = Array.from(relaySet);
|
||||
const readRelays = relayManager.getReadRelayUrls();
|
||||
const relays = readRelays.length > 0 ? readRelays : Array.from(DEFAULT_RELAY_URLS);
|
||||
if (!relays.length) {
|
||||
if (wasLoadedForUser) {
|
||||
userLogger.warn(
|
||||
|
||||
+3
-30
@@ -11,6 +11,7 @@ import {
|
||||
subscribeVideoViewEvents,
|
||||
} from "./nostrViewEventsFacade.js";
|
||||
import { DEFAULT_RELAY_URLS } from "./nostr/toolkit.js";
|
||||
import { relayManager } from "./relayManager.js";
|
||||
import {
|
||||
buildSubscriptionListEvent,
|
||||
SUBSCRIPTION_LIST_IDENTIFIER,
|
||||
@@ -429,36 +430,8 @@ class SubscriptionsManager {
|
||||
try {
|
||||
const normalizedUserPubkey = normalizeHexPubkey(userPubkey) || userPubkey;
|
||||
|
||||
const relaySet = new Set();
|
||||
const addRelayCandidates = (candidates) => {
|
||||
if (!candidates) {
|
||||
return;
|
||||
}
|
||||
const iterable = Array.isArray(candidates)
|
||||
? candidates
|
||||
: candidates instanceof Set
|
||||
? Array.from(candidates)
|
||||
: [];
|
||||
for (const candidate of iterable) {
|
||||
if (typeof candidate !== "string") {
|
||||
continue;
|
||||
}
|
||||
const trimmed = candidate.trim();
|
||||
if (trimmed) {
|
||||
relaySet.add(trimmed);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
addRelayCandidates(nostrClient.relays);
|
||||
if (!relaySet.size) {
|
||||
addRelayCandidates(nostrClient.readRelays);
|
||||
}
|
||||
if (!relaySet.size) {
|
||||
addRelayCandidates(DEFAULT_RELAY_URLS);
|
||||
}
|
||||
|
||||
const relayUrls = Array.from(relaySet);
|
||||
const readRelays = relayManager.getReadRelayUrls();
|
||||
const relayUrls = readRelays.length > 0 ? readRelays : Array.from(DEFAULT_RELAY_URLS);
|
||||
if (!relayUrls.length) {
|
||||
devLogger.warn(
|
||||
"[SubscriptionsManager] No relay URLs available while loading subscriptions.",
|
||||
|
||||
+3
-20
@@ -19,6 +19,7 @@ import {
|
||||
} from "./nostrPublish.js";
|
||||
import { profileCache } from "./state/profileCache.js";
|
||||
import { DEFAULT_RELAY_URLS } from "./nostr/toolkit.js";
|
||||
import { relayManager } from "./relayManager.js";
|
||||
|
||||
class TinyEventEmitter {
|
||||
constructor() {
|
||||
@@ -768,26 +769,8 @@ class UserBlockListManager {
|
||||
}
|
||||
};
|
||||
|
||||
const relaySet = new Set();
|
||||
const addRelays = (candidate) => {
|
||||
const sanitized = sanitizeRelayList(candidate);
|
||||
for (const relay of sanitized) {
|
||||
relaySet.add(relay);
|
||||
}
|
||||
};
|
||||
|
||||
addRelays(nostrClient.relays);
|
||||
if (!relaySet.size) {
|
||||
addRelays(nostrClient.readRelays);
|
||||
}
|
||||
if (!relaySet.size) {
|
||||
addRelays(nostrClient.writeRelays);
|
||||
}
|
||||
if (!relaySet.size) {
|
||||
addRelays(DEFAULT_RELAY_URLS);
|
||||
}
|
||||
|
||||
const relays = Array.from(relaySet);
|
||||
const readRelays = relayManager.getReadRelayUrls();
|
||||
const relays = readRelays.length > 0 ? readRelays : Array.from(DEFAULT_RELAY_URLS);
|
||||
emitStatus({ status: "loading", relays });
|
||||
|
||||
const localBlocks = this._loadLocal(normalized);
|
||||
|
||||
@@ -10,6 +10,7 @@ const [{ nostrClient }, { setActiveSigner, clearActiveSigner }] =
|
||||
import("../js/nostrClientFacade.js"),
|
||||
import("../js/nostr/client.js"),
|
||||
]);
|
||||
const { relayManager } = await import("../js/relayManager.js");
|
||||
|
||||
if (typeof globalThis.window === "undefined") {
|
||||
globalThis.window = {};
|
||||
@@ -22,6 +23,7 @@ const originalRelays = Array.isArray(nostrClient.relays)
|
||||
const originalWriteRelays = Array.isArray(nostrClient.writeRelays)
|
||||
? [...nostrClient.writeRelays]
|
||||
: nostrClient.writeRelays;
|
||||
const originalRelayEntries = relayManager.getEntries();
|
||||
const originalWindowNostr = window.nostr;
|
||||
|
||||
function restoreNostrClient() {
|
||||
@@ -34,9 +36,14 @@ function restoreNostrClient() {
|
||||
: originalWriteRelays;
|
||||
}
|
||||
|
||||
function restoreRelayManager() {
|
||||
relayManager.setEntries(originalRelayEntries, { allowEmpty: true, updateClient: false });
|
||||
}
|
||||
|
||||
test.beforeEach(async () => {
|
||||
hashtagPreferences.reset();
|
||||
restoreNostrClient();
|
||||
restoreRelayManager();
|
||||
window.nostr = originalWindowNostr;
|
||||
|
||||
// Setup default mock signer to prevent auto-save errors
|
||||
@@ -68,6 +75,7 @@ test.beforeEach(async () => {
|
||||
|
||||
test.after(() => {
|
||||
restoreNostrClient();
|
||||
restoreRelayManager();
|
||||
window.nostr = originalWindowNostr;
|
||||
clearActiveSigner();
|
||||
});
|
||||
@@ -116,6 +124,7 @@ test(
|
||||
};
|
||||
nostrClient.relays = ["wss://relay.one"];
|
||||
nostrClient.writeRelays = ["wss://relay.one"];
|
||||
relayManager.setEntries([{ url: "wss://relay.one", mode: "both" }], { allowEmpty: false, updateClient: false });
|
||||
|
||||
await hashtagPreferences.load(pubkey);
|
||||
|
||||
@@ -156,6 +165,7 @@ test(
|
||||
};
|
||||
nostrClient.relays = ["wss://relay.two"];
|
||||
nostrClient.writeRelays = ["wss://relay.two"];
|
||||
relayManager.setEntries([{ url: "wss://relay.two", mode: "both" }], { allowEmpty: false, updateClient: false });
|
||||
|
||||
await hashtagPreferences.load(pubkey);
|
||||
|
||||
@@ -201,6 +211,7 @@ test(
|
||||
};
|
||||
nostrClient.relays = ["wss://relay.retry"];
|
||||
nostrClient.writeRelays = ["wss://relay.retry"];
|
||||
relayManager.setEntries([{ url: "wss://relay.retry", mode: "both" }], { allowEmpty: false, updateClient: false });
|
||||
|
||||
await hashtagPreferences.load(pubkey);
|
||||
|
||||
@@ -259,6 +270,7 @@ test(
|
||||
};
|
||||
nostrClient.relays = ["wss://relay.publish"];
|
||||
nostrClient.writeRelays = ["wss://relay.publish"];
|
||||
relayManager.setEntries([{ url: "wss://relay.publish", mode: "both" }], { allowEmpty: false, updateClient: false });
|
||||
|
||||
await hashtagPreferences.load(pubkey);
|
||||
hashtagPreferences.addInterest("TagA");
|
||||
@@ -331,6 +343,7 @@ test(
|
||||
};
|
||||
nostrClient.relays = ["wss://relay.window"];
|
||||
nostrClient.writeRelays = ["wss://relay.window"];
|
||||
relayManager.setEntries([{ url: "wss://relay.window", mode: "both" }], { allowEmpty: false, updateClient: false });
|
||||
|
||||
await hashtagPreferences.load(pubkey);
|
||||
hashtagPreferences.addInterest("WindowTag");
|
||||
@@ -420,6 +433,7 @@ test(
|
||||
};
|
||||
nostrClient.relays = ["wss://relay.tie"];
|
||||
nostrClient.writeRelays = ["wss://relay.tie"];
|
||||
relayManager.setEntries([{ url: "wss://relay.tie", mode: "both" }], { allowEmpty: false, updateClient: false });
|
||||
|
||||
await hashtagPreferences.load(pubkey);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import nostrService from "../js/services/nostrService.js";
|
||||
import moderationService from "../js/services/moderationService.js";
|
||||
import { getNostrEventSchema, NOTE_TYPES } from "../js/nostrEventSchemas.js";
|
||||
import { profileCache } from "../js/state/profileCache.js";
|
||||
import { relayManager } from "../js/relayManager.js";
|
||||
|
||||
test("loadSubscriptions aggregates relay results when one rejects", async () => {
|
||||
const SubscriptionsManager = subscriptions.constructor;
|
||||
@@ -39,6 +40,9 @@ test("loadSubscriptions aggregates relay results when one rejects", async () =>
|
||||
nostrClient.relays = relayUrls;
|
||||
nostrClient.writeRelays = relayUrls;
|
||||
|
||||
const originalRelayEntries = relayManager.getEntries();
|
||||
relayManager.setEntries(relayUrls.map(url => ({ url, mode: "both" })), { allowEmpty: false, updateClient: false });
|
||||
|
||||
const eventsByRelay = {
|
||||
"wss://relay-a.example": [
|
||||
{
|
||||
@@ -120,6 +124,7 @@ test("loadSubscriptions aggregates relay results when one rejects", async () =>
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
relayManager.setEntries(originalRelayEntries, { allowEmpty: false, updateClient: false });
|
||||
nostrClient.relays = originalRelays;
|
||||
nostrClient.writeRelays = originalWriteRelays;
|
||||
nostrClient.pool = originalPool;
|
||||
|
||||
@@ -8,6 +8,7 @@ const {
|
||||
const { setActiveSigner, clearActiveSigner, getActiveSigner } = await import(
|
||||
"../js/nostr/client.js"
|
||||
);
|
||||
const { relayManager } = await import("../js/relayManager.js");
|
||||
|
||||
if (typeof globalThis.window === "undefined") {
|
||||
globalThis.window = {};
|
||||
@@ -33,6 +34,9 @@ await (async () => {
|
||||
"wss://relay-three.example",
|
||||
];
|
||||
|
||||
const originalRelayEntries = relayManager.getEntries();
|
||||
relayManager.setEntries(relays.map(url => ({ url, mode: "both" })), { allowEmpty: false, updateClient: false });
|
||||
|
||||
const originalNostr = window.nostr;
|
||||
const originalPool = nostrClient.pool;
|
||||
const originalRelays = Array.isArray(nostrClient.relays)
|
||||
@@ -147,6 +151,7 @@ await (async () => {
|
||||
"decrypted block list should hydrate from newest event"
|
||||
);
|
||||
} finally {
|
||||
relayManager.setEntries(originalRelayEntries, { allowEmpty: false, updateClient: false });
|
||||
userBlocks.blockedPubkeys = originalBlocked;
|
||||
userBlocks.blockEventId = originalBlockEventId;
|
||||
userBlocks.muteEventId = originalMuteEventId;
|
||||
@@ -170,6 +175,9 @@ await (async () => {
|
||||
"wss://background.example",
|
||||
];
|
||||
|
||||
const originalRelayEntries = relayManager.getEntries();
|
||||
relayManager.setEntries(relays.map(url => ({ url, mode: "both" })), { allowEmpty: false, updateClient: false });
|
||||
|
||||
const originalNostr = window.nostr;
|
||||
const originalPool = nostrClient.pool;
|
||||
const originalRelays = Array.isArray(nostrClient.relays)
|
||||
@@ -302,6 +310,7 @@ await (async () => {
|
||||
} finally {
|
||||
unsubscribeStatus?.();
|
||||
unsubscribeChange?.();
|
||||
relayManager.setEntries(originalRelayEntries, { allowEmpty: false, updateClient: false });
|
||||
userBlocks.blockedPubkeys = originalBlocked;
|
||||
userBlocks.blockEventId = originalBlockEventId;
|
||||
userBlocks.blockEventCreatedAt = originalBlockEventCreatedAt;
|
||||
@@ -520,7 +529,11 @@ await (async () => {
|
||||
|
||||
const signedEvents = [];
|
||||
|
||||
nostrClient.relays = ["wss://direct-signer.example"];
|
||||
const relayUrls = ["wss://direct-signer.example"];
|
||||
const originalRelayEntries = relayManager.getEntries();
|
||||
relayManager.setEntries(relayUrls.map(url => ({ url, mode: "both" })), { allowEmpty: false, updateClient: false });
|
||||
|
||||
nostrClient.relays = relayUrls;
|
||||
nostrClient.writeRelays = nostrClient.relays;
|
||||
nostrClient.pool = {
|
||||
publish: (_targets, event) => {
|
||||
@@ -662,6 +675,7 @@ await (async () => {
|
||||
"mute list event should include p-tags for blocked pubkeys",
|
||||
);
|
||||
} finally {
|
||||
relayManager.setEntries(originalRelayEntries, { allowEmpty: false, updateClient: false });
|
||||
userBlocks.blockedPubkeys = originalBlocked;
|
||||
userBlocks.blockEventId = originalBlockEventId;
|
||||
userBlocks.blockEventCreatedAt = originalBlockEventCreatedAt;
|
||||
@@ -866,6 +880,9 @@ await (async () => {
|
||||
clearActiveSigner();
|
||||
|
||||
const relayUrls = ["wss://relay-nip44-read.example"];
|
||||
const originalRelayEntries = relayManager.getEntries();
|
||||
relayManager.setEntries(relayUrls.map(url => ({ url, mode: "both" })), { allowEmpty: false, updateClient: false });
|
||||
|
||||
nostrClient.relays = relayUrls;
|
||||
nostrClient.ensureExtensionPermissions = async () => ({ ok: true });
|
||||
|
||||
@@ -908,6 +925,7 @@ await (async () => {
|
||||
assert.equal(decryptCalls.nip44, 1, "nip44 decryptor should be preferred");
|
||||
assert.equal(decryptCalls.nip04, 0, "nip04 decryptor should not be used");
|
||||
} finally {
|
||||
relayManager.setEntries(originalRelayEntries, { allowEmpty: false, updateClient: false });
|
||||
window.nostr = originalNostr;
|
||||
nostrClient.relays = originalRelays;
|
||||
nostrClient.pool = originalPool;
|
||||
@@ -1088,7 +1106,11 @@ await (async () => {
|
||||
const originalRelays = Array.isArray(nostrClient.relays) ? [...nostrClient.relays] : nostrClient.relays;
|
||||
const originalPool = nostrClient.pool;
|
||||
|
||||
nostrClient.relays = ["wss://tag-only.example"];
|
||||
const relayUrls = ["wss://tag-only.example"];
|
||||
const originalRelayEntries = relayManager.getEntries();
|
||||
relayManager.setEntries(relayUrls.map(url => ({ url, mode: "both" })), { allowEmpty: false, updateClient: false });
|
||||
|
||||
nostrClient.relays = relayUrls;
|
||||
nostrClient.pool = {
|
||||
list: async () => [
|
||||
{
|
||||
@@ -1109,6 +1131,7 @@ await (async () => {
|
||||
assert.equal(manager.blockedPubkeys.size, 1, "should have exactly one blocked pubkey");
|
||||
assert.equal(manager.blockEventId, "event-tag-only", "should track the correct event ID");
|
||||
} finally {
|
||||
relayManager.setEntries(originalRelayEntries, { allowEmpty: false, updateClient: false });
|
||||
nostrClient.relays = originalRelays;
|
||||
nostrClient.pool = originalPool;
|
||||
}
|
||||
@@ -1125,7 +1148,11 @@ await (async () => {
|
||||
const originalRelays = Array.isArray(nostrClient.relays) ? [...nostrClient.relays] : nostrClient.relays;
|
||||
const originalPool = nostrClient.pool;
|
||||
|
||||
nostrClient.relays = ["wss://tag-only-whitespace.example"];
|
||||
const relayUrls = ["wss://tag-only-whitespace.example"];
|
||||
const originalRelayEntries = relayManager.getEntries();
|
||||
relayManager.setEntries(relayUrls.map(url => ({ url, mode: "both" })), { allowEmpty: false, updateClient: false });
|
||||
|
||||
nostrClient.relays = relayUrls;
|
||||
nostrClient.pool = {
|
||||
list: async () => [
|
||||
{
|
||||
@@ -1144,6 +1171,7 @@ await (async () => {
|
||||
|
||||
assert(manager.blockedPubkeys.has(blockedHex), "should load blocked pubkeys from p-tags when content is whitespace");
|
||||
} finally {
|
||||
relayManager.setEntries(originalRelayEntries, { allowEmpty: false, updateClient: false });
|
||||
nostrClient.relays = originalRelays;
|
||||
nostrClient.pool = originalPool;
|
||||
}
|
||||
@@ -1161,7 +1189,11 @@ await (async () => {
|
||||
const originalRelays = Array.isArray(nostrClient.relays) ? [...nostrClient.relays] : nostrClient.relays;
|
||||
const originalPool = nostrClient.pool;
|
||||
|
||||
nostrClient.relays = ["wss://empty-clear.example"];
|
||||
const relayUrls = ["wss://empty-clear.example"];
|
||||
const originalRelayEntries = relayManager.getEntries();
|
||||
relayManager.setEntries(relayUrls.map(url => ({ url, mode: "both" })), { allowEmpty: false, updateClient: false });
|
||||
|
||||
nostrClient.relays = relayUrls;
|
||||
nostrClient.pool = {
|
||||
list: async () => [
|
||||
{
|
||||
@@ -1180,6 +1212,7 @@ await (async () => {
|
||||
|
||||
assert.equal(manager.blockedPubkeys.size, 0, "should clear block list when content and tags are empty");
|
||||
} finally {
|
||||
relayManager.setEntries(originalRelayEntries, { allowEmpty: false, updateClient: false });
|
||||
nostrClient.relays = originalRelays;
|
||||
nostrClient.pool = originalPool;
|
||||
}
|
||||
@@ -1195,7 +1228,11 @@ await (async () => {
|
||||
const originalPool = nostrClient.pool;
|
||||
const originalNostr = window.nostr;
|
||||
|
||||
nostrClient.relays = ["wss://repro.example"];
|
||||
const relayUrls = ["wss://repro.example"];
|
||||
const originalRelayEntries = relayManager.getEntries();
|
||||
relayManager.setEntries(relayUrls.map(url => ({ url, mode: "both" })), { allowEmpty: false, updateClient: false });
|
||||
|
||||
nostrClient.relays = relayUrls;
|
||||
|
||||
window.nostr = {
|
||||
nip04: {
|
||||
@@ -1238,6 +1275,7 @@ await (async () => {
|
||||
assert.ok(blocked.includes(taggedBlocked), "Should include pubkey from tagged list");
|
||||
assert.ok(!blocked.includes(plainBlocked), "Should NOT include pubkey from plain list (fallback ignored)");
|
||||
} finally {
|
||||
relayManager.setEntries(originalRelayEntries, { allowEmpty: false, updateClient: false });
|
||||
nostrClient.relays = originalRelays;
|
||||
nostrClient.pool = originalPool;
|
||||
window.nostr = originalNostr;
|
||||
|
||||
Reference in new issue
Block a user