fix(client): track and filter unreachable relays to prevent timeouts

Introduces `unreachableRelays` tracking in `NostrClient` to identify relays that fail connection during initialization.
Updates fetch operations (`fetchListIncrementally`, `subscribeVideos`, etc.) to use `getHealthyRelays`, filtering out known bad relays to prevent `SimplePool` timeouts (7s) from blocking the application.
This resolves slow loading and app breakage when the user's persistent configuration contains dead relays.

Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
google-labs-jules[bot]andPR0M3TH3AN committed 2026-01-26 19:55:05 +00:00
1 parent 4a9d771689
commit c81f71b6d2
1 file changed
+31 -17
+31 -17
View File
@@ -1220,6 +1220,7 @@ export class NostrClient {
this.sessionActorCipherClosures = null;
this.sessionActorCipherClosuresPrivateKey = null;
this.countUnsupportedRelays = new Set();
this.unreachableRelays = new Set();
this.isInitialized = false;
this.nip46Client = null;
this.remoteSignerListeners = new Set();
@@ -3503,7 +3504,7 @@ export class NostrClient {
? relayUrls
: this.relays;
const normalizedRelays = sanitizeRelayList(relaysToUse);
const normalizedRelays = sanitizeRelayList(this.getHealthyRelays(relaysToUse));
const results = [];
const concurrencyLimit = 4;
@@ -3943,7 +3944,7 @@ export class NostrClient {
// `unsub` to avoid leaking subscriptions. Note: any future change must still
// provide a lightweight readiness check with similar timeout semantics.
async connectToRelays() {
return Promise.all(
const results = await Promise.all(
this.relays.map(
(url) =>
new Promise((resolve) => {
@@ -3963,6 +3964,27 @@ export class NostrClient {
})
)
);
for (const result of results) {
if (result.success) {
this.unreachableRelays.delete(result.url);
} else {
this.unreachableRelays.add(result.url);
if (isDevMode) {
devLogger.warn(`[nostr] Marked relay as unreachable: ${result.url}`);
}
}
}
return results;
}
getHealthyRelays(candidates) {
const source = Array.isArray(candidates) ? candidates : this.relays;
if (!this.unreachableRelays.size) {
return source;
}
return source.filter((url) => !this.unreachableRelays.has(url));
}
/**
@@ -4426,7 +4448,7 @@ export class NostrClient {
: Array.isArray(this.readRelays) && this.readRelays.length
? this.readRelays
: this.relays;
const relays = sanitizeRelayList(relayCandidates);
const relays = sanitizeRelayList(this.getHealthyRelays(relayCandidates));
const relaysToUse = relays.length ? relays : Array.from(DEFAULT_RELAY_URLS);
const filters = buildDmFilters(
@@ -4498,7 +4520,7 @@ export class NostrClient {
: Array.isArray(this.readRelays) && this.readRelays.length
? this.readRelays
: this.relays;
const relays = sanitizeRelayList(relayCandidates);
const relays = sanitizeRelayList(this.getHealthyRelays(relayCandidates));
const relaysToUse = relays.length ? relays : Array.from(DEFAULT_RELAY_URLS);
const filters = buildDmFilters(actorPubkeyInput, options);
@@ -6749,7 +6771,7 @@ export class NostrClient {
devLogger.log("[subscribeVideos] Subscribing with filter:", filter);
const sub = this.pool.sub(this.relays, [filter]);
const sub = this.pool.sub(this.getHealthyRelays(this.relays), [filter]);
const invalidDuringSub = [];
// BUFFERING STATE
@@ -7017,7 +7039,7 @@ export class NostrClient {
try {
await Promise.all(
this.relays.map(async (url) => {
this.getHealthyRelays(this.relays).map(async (url) => {
const events = await this.pool.list([url], [filter]);
for (const evt of events) {
if (evt && evt.id) {
@@ -7117,15 +7139,7 @@ export class NostrClient {
Array.isArray(options?.relays) && options.relays.length
? options.relays
: this.relays;
const relays = Array.isArray(relayCandidatesRaw)
? Array.from(
new Set(
relayCandidatesRaw
.map((url) => (typeof url === "string" ? url.trim() : ""))
.filter(Boolean)
)
)
: [];
const relays = sanitizeRelayList(this.getHealthyRelays(relayCandidatesRaw));
if (!relays.length) {
return null;
@@ -7765,7 +7779,7 @@ export class NostrClient {
}
try {
const rootEvent = await this.pool.get(this.relays, { ids: [normalizedRoot] });
const rootEvent = await this.pool.get(this.getHealthyRelays(this.relays), { ids: [normalizedRoot] });
if (rootEvent && rootEvent.id === normalizedRoot) {
this.rawEvents.set(rootEvent.id, rootEvent);
const parsed = convertEventToVideo(rootEvent);
@@ -7808,7 +7822,7 @@ export class NostrClient {
try {
const perRelay = await Promise.all(
this.relays.map(async (url) => {
this.getHealthyRelays(this.relays).map(async (url) => {
try {
const events = await this.pool.list([url], [filter]);
return events || [];