From 43a7dcd14ca5154aa83dd5ca4d62e661ddc85f7f Mon Sep 17 00:00:00 2001 From: silberengel Date: Thu, 14 Aug 2025 08:00:42 +0200 Subject: [PATCH] fixed relay connections --- src/lib/consts.ts | 9 +- src/lib/ndk.ts | 150 ++++++++++++----- src/lib/stores/userStore.ts | 4 +- src/lib/utils/relay_management.ts | 263 +++++++++++++++++++++++------- 4 files changed, 324 insertions(+), 102 deletions(-) diff --git a/src/lib/consts.ts b/src/lib/consts.ts index b8e7f0d..4f78a56 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -32,9 +32,7 @@ export const secondaryRelays = [ export const anonymousRelays = [ "wss://freelay.sovbit.host", - "wss://thecitadel.nostr1.com", - "wss://relay.damus.io", - "wss://relay.nostr.band" + "wss://thecitadel.nostr1.com" ]; export const lowbandwidthRelays = [ @@ -44,8 +42,9 @@ export const lowbandwidthRelays = [ ]; export const localRelays: string[] = [ - "wss://localhost:8080", - "wss://localhost:4869", + "ws://localhost:8080", + "ws://localhost:4869", + "ws://localhost:3334" ]; export enum FeedType { diff --git a/src/lib/ndk.ts b/src/lib/ndk.ts index f89a9f7..45e9097 100644 --- a/src/lib/ndk.ts +++ b/src/lib/ndk.ts @@ -9,6 +9,7 @@ import NDK, { import { writable, get, type Writable } from "svelte/store"; import { loginStorageKey, + anonymousRelays, } from "./consts.ts"; import { buildCompleteRelaySet, @@ -91,8 +92,18 @@ function clearPersistentRelaySet(): void { } // Subscribe to userStore changes and update ndkSignedIn accordingly -userStore.subscribe((userState) => { +userStore.subscribe(async (userState) => { ndkSignedIn.set(userState.signedIn); + + // Refresh relay stores when user state changes + const ndk = get(ndkInstance); + if (ndk) { + try { + await refreshRelayStores(ndk); + } catch (error) { + console.warn('[NDK.ts] Failed to refresh relay stores on user state change:', error); + } + } }); /** @@ -322,15 +333,21 @@ export function clearPersistedRelays(user: NDKUser): void { /** * Ensures a relay URL uses secure WebSocket protocol * @param url The relay URL to secure - * @returns The URL with wss:// protocol + * @returns The URL with appropriate protocol (ws:// for localhost, wss:// for remote) */ function ensureSecureWebSocket(url: string): string { - // Replace ws:// with wss:// if present + // For localhost, always use ws:// (never wss://) + if (url.includes('localhost') || url.includes('127.0.0.1')) { + // Convert any wss://localhost to ws://localhost + return url.replace(/^wss:\/\//, "ws://"); + } + + // Replace ws:// with wss:// for remote relays const secureUrl = url.replace(/^ws:\/\//, "wss://"); if (secureUrl !== url) { console.warn( - `[NDK.ts] Protocol downgrade detected: ${url} -> ${secureUrl}`, + `[NDK.ts] Protocol upgrade for remote relay: ${url} -> ${secureUrl}`, ); } @@ -341,46 +358,85 @@ function ensureSecureWebSocket(url: string): string { * Creates a relay with proper authentication handling */ function createRelayWithAuth(url: string, ndk: NDK): NDKRelay { - console.debug(`[NDK.ts] Creating relay with URL: ${url}`); + try { + console.debug(`[NDK.ts] Creating relay with URL: ${url}`); - // Ensure the URL is using wss:// protocol - const secureUrl = ensureSecureWebSocket(url); + // Ensure the URL is using appropriate protocol + const secureUrl = ensureSecureWebSocket(url); - // Add connection timeout and error handling - const relay = new NDKRelay( - secureUrl, - NDKRelayAuthPolicies.signIn({ ndk }), - ndk, - ); + // Add connection timeout and error handling + const relay = new NDKRelay( + secureUrl, + NDKRelayAuthPolicies.signIn({ ndk }), + ndk, + ); - // Set up connection timeout - const connectionTimeout = setTimeout(() => { - console.warn(`[NDK.ts] Connection timeout for ${secureUrl}`); - relay.disconnect(); - }, 5000); // 5 second timeout - - // Set up custom authentication handling only if user is signed in - if (ndk.signer && ndk.activeUser) { - const authPolicy = new CustomRelayAuthPolicy(ndk); - relay.on("connect", () => { - console.debug(`[NDK.ts] Relay connected: ${secureUrl}`); - clearTimeout(connectionTimeout); - authPolicy.authenticate(relay); - }); - } else { - relay.on("connect", () => { - console.debug(`[NDK.ts] Relay connected: ${secureUrl}`); - clearTimeout(connectionTimeout); - }); - } + // Set up connection timeout + const connectionTimeout = setTimeout(() => { + try { + console.warn(`[NDK.ts] Connection timeout for ${secureUrl}`); + relay.disconnect(); + } catch { + // Silently ignore disconnect errors + } + }, 5000); // 5 second timeout - // Add error handling - relay.on("disconnect", () => { - console.debug(`[NDK.ts] Relay disconnected: ${secureUrl}`); - clearTimeout(connectionTimeout); - }); + // Set up custom authentication handling only if user is signed in + if (ndk.signer && ndk.activeUser) { + const authPolicy = new CustomRelayAuthPolicy(ndk); + relay.on("connect", () => { + try { + console.debug(`[NDK.ts] Relay connected: ${secureUrl}`); + clearTimeout(connectionTimeout); + authPolicy.authenticate(relay); + } catch { + // Silently handle connect handler errors + } + }); + } else { + relay.on("connect", () => { + try { + console.debug(`[NDK.ts] Relay connected: ${secureUrl}`); + clearTimeout(connectionTimeout); + } catch { + // Silently handle connect handler errors + } + }); + } + + // Add error handling + relay.on("disconnect", () => { + try { + console.debug(`[NDK.ts] Relay disconnected: ${secureUrl}`); + clearTimeout(connectionTimeout); + } catch { + // Silently handle disconnect handler errors + } + }); - return relay; + return relay; + } catch (error) { + // If relay creation fails, try to use an anonymous relay as fallback + console.debug(`[NDK.ts] Failed to create relay for ${url}, trying anonymous relay fallback`); + + // Find an anonymous relay that's not the same as the failed URL + const fallbackUrl = anonymousRelays.find(relay => relay !== url) || anonymousRelays[0]; + + if (fallbackUrl) { + console.debug(`[NDK.ts] Using anonymous relay as fallback: ${fallbackUrl}`); + try { + const fallbackRelay = new NDKRelay(fallbackUrl, NDKRelayAuthPolicies.signIn({ ndk }), ndk); + return fallbackRelay; + } catch (fallbackError) { + console.debug(`[NDK.ts] Fallback relay creation also failed: ${fallbackError}`); + } + } + + // If all else fails, create a minimal relay that will fail gracefully + console.debug(`[NDK.ts] All fallback attempts failed, creating minimal relay for ${url}`); + const minimalRelay = new NDKRelay(url, undefined, ndk); + return minimalRelay; + } } @@ -478,13 +534,27 @@ export function logCurrentRelayConfiguration(): void { console.log(`📊 Total: ${inboxRelays.length} inbox, ${outboxRelays.length} outbox`); } +/** + * Clears the relay set cache to force a rebuild + */ +export function clearRelaySetCache(): void { + console.debug('[NDK.ts] Clearing relay set cache'); + persistentRelaySet = null; + relaySetLastUpdated = 0; + // Clear from localStorage as well + if (typeof localStorage !== 'undefined') { + localStorage.removeItem('alexandria/relay_set_cache'); + } +} + /** * Updates relay stores when user state changes * @param ndk NDK instance */ export async function refreshRelayStores(ndk: NDK): Promise { console.debug('[NDK.ts] Refreshing relay stores due to user state change'); - await updateActiveRelayStores(ndk); + clearRelaySetCache(); // Clear cache when user state changes + await updateActiveRelayStores(ndk, true); // Force update } /** diff --git a/src/lib/stores/userStore.ts b/src/lib/stores/userStore.ts index 1e58f42..6189158 100644 --- a/src/lib/stores/userStore.ts +++ b/src/lib/stores/userStore.ts @@ -206,7 +206,7 @@ export async function loginWithExtension() { // Update relay stores with the new user's relays try { console.debug('[userStore.ts] loginWithExtension: Updating relay stores for authenticated user'); - await updateActiveRelayStores(ndk); + await updateActiveRelayStores(ndk, true); // Force update to rebuild relay set for authenticated user } catch (error) { console.warn('[userStore.ts] loginWithExtension: Failed to update relay stores:', error); } @@ -273,7 +273,7 @@ export async function loginWithAmber(amberSigner: NDKSigner, user: NDKUser) { // Update relay stores with the new user's relays try { console.debug('[userStore.ts] loginWithAmber: Updating relay stores for authenticated user'); - await updateActiveRelayStores(ndk); + await updateActiveRelayStores(ndk, true); // Force update to rebuild relay set for authenticated user } catch (error) { console.warn('[userStore.ts] loginWithAmber: Failed to update relay stores:', error); } diff --git a/src/lib/utils/relay_management.ts b/src/lib/utils/relay_management.ts index a4f41fa..c9c10a0 100644 --- a/src/lib/utils/relay_management.ts +++ b/src/lib/utils/relay_management.ts @@ -43,12 +43,12 @@ export function deduplicateRelayUrls(urls: string[]): string[] { } /** - * Tests connection to a relay and returns connection status - * @param relayUrl The relay URL to test + * Tests connection to a local relay (ws:// protocol) + * @param relayUrl The local relay URL to test (should be ws://) * @param ndk The NDK instance * @returns Promise that resolves to connection status */ -export function testRelayConnection( +export function testLocalRelayConnection( relayUrl: string, ndk: NDK, ): Promise<{ @@ -58,8 +58,135 @@ export function testRelayConnection( actualUrl?: string; }> { return new Promise((resolve) => { - // Ensure the URL is using wss:// protocol - const secureUrl = ensureSecureWebSocket(relayUrl); + try { + // Ensure the URL is using ws:// protocol for local relays + const localUrl = relayUrl.replace(/^wss:\/\//, "ws://"); + + // Use the existing NDK instance instead of creating a new one + const relay = new NDKRelay(localUrl, undefined, ndk); + let authRequired = false; + let connected = false; + let error: string | undefined; + let actualUrl: string | undefined; + + const timeout = setTimeout(() => { + try { + relay.disconnect(); + } catch { + // Silently ignore disconnect errors + } + resolve({ + connected: false, + requiresAuth: authRequired, + error: "Connection timeout", + actualUrl, + }); + }, 3000); + + // Wrap all event handlers in try-catch to prevent errors from bubbling up + relay.on("connect", () => { + try { + connected = true; + actualUrl = localUrl; + clearTimeout(timeout); + relay.disconnect(); + resolve({ + connected: true, + requiresAuth: authRequired, + error, + actualUrl, + }); + } catch { + // Silently handle any errors in connect handler + clearTimeout(timeout); + resolve({ + connected: false, + requiresAuth: false, + error: "Connection handler error", + actualUrl: localUrl, + }); + } + }); + + relay.on("notice", (message: string) => { + try { + if (message.includes("auth-required")) { + authRequired = true; + } + } catch { + // Silently ignore notice handler errors + } + }); + + relay.on("disconnect", () => { + try { + if (!connected) { + error = "Connection failed"; + clearTimeout(timeout); + resolve({ + connected: false, + requiresAuth: authRequired, + error, + actualUrl, + }); + } + } catch { + // Silently handle any errors in disconnect handler + clearTimeout(timeout); + resolve({ + connected: false, + requiresAuth: false, + error: "Disconnect handler error", + actualUrl: localUrl, + }); + } + }); + + // Wrap the connect call in try-catch + try { + relay.connect(); + } catch (connectError) { + // Silently handle connection errors + clearTimeout(timeout); + resolve({ + connected: false, + requiresAuth: false, + error: "Connection failed", + actualUrl: localUrl, + }); + } + } catch (outerError) { + // Catch any other errors that might occur during setup + resolve({ + connected: false, + requiresAuth: false, + error: "Setup failed", + actualUrl: relayUrl, + }); + } + }); +} + +/** + * Tests connection to a remote relay (wss:// protocol) + * @param relayUrl The remote relay URL to test + * @param ndk The NDK instance + * @returns Promise that resolves to connection status + */ +export function testRemoteRelayConnection( + relayUrl: string, + ndk: NDK, +): Promise<{ + connected: boolean; + requiresAuth: boolean; + error?: string; + actualUrl?: string; +}> { + return new Promise((resolve) => { + // Ensure the URL is using wss:// protocol for remote relays + const secureUrl = relayUrl.replace(/^ws:\/\//, "wss://"); + + console.debug(`[relay_management.ts] Testing remote relay connection: ${secureUrl}`); // Use the existing NDK instance instead of creating a new one const relay = new NDKRelay(secureUrl, undefined, ndk); @@ -69,6 +196,7 @@ export function testRelayConnection( let actualUrl: string | undefined; const timeout = setTimeout(() => { + console.debug(`[relay_management.ts] Relay ${secureUrl} connection timeout`); relay.disconnect(); resolve({ connected: false, @@ -76,9 +204,10 @@ export function testRelayConnection( error: "Connection timeout", actualUrl, }); - }, 3000); // Increased timeout to 3 seconds to give relays more time + }, 3000); relay.on("connect", () => { + console.debug(`[relay_management.ts] Relay ${secureUrl} connected successfully`); connected = true; actualUrl = secureUrl; clearTimeout(timeout); @@ -99,6 +228,7 @@ export function testRelayConnection( relay.on("disconnect", () => { if (!connected) { + console.debug(`[relay_management.ts] Relay ${secureUrl} disconnected without connecting`); error = "Connection failed"; clearTimeout(timeout); resolve({ @@ -113,30 +243,31 @@ export function testRelayConnection( relay.connect(); }); } - + /** - * Ensures a relay URL uses secure WebSocket protocol for remote relays - * @param url The relay URL to secure - * @returns The URL with wss:// protocol (except for localhost) + * Tests connection to a relay and returns connection status + * @param relayUrl The relay URL to test + * @param ndk The NDK instance + * @returns Promise that resolves to connection status */ -function ensureSecureWebSocket(url: string): string { - // For localhost, always use ws:// (never wss://) - if (url.includes('localhost') || url.includes('127.0.0.1')) { - // Convert any wss://localhost to ws://localhost - return url.replace(/^wss:\/\//, "ws://"); - } - - // Replace ws:// with wss:// for remote relays - const secureUrl = url.replace(/^ws:\/\//, "wss://"); - - if (secureUrl !== url) { - console.warn( - `[relay_management.ts] Protocol upgrade for rem ote relay: ${url} -> ${secureUrl}`, - ); +export function testRelayConnection( + relayUrl: string, + ndk: NDK, +): Promise<{ + connected: boolean; + requiresAuth: boolean; + error?: string; + actualUrl?: string; +}> { + // Determine if this is a local or remote relay + if (relayUrl.includes('localhost') || relayUrl.includes('127.0.0.1')) { + return testLocalRelayConnection(relayUrl, ndk); + } else { + return testRemoteRelayConnection(relayUrl, ndk); } - - return secureUrl; } + + /** * Tests connection to local relays @@ -145,33 +276,38 @@ function ensureSecureWebSocket(url: string): string { * @returns Promise that resolves to array of working local relay URLs */ async function testLocalRelays(localRelayUrls: string[], ndk: NDK): Promise { - const workingRelays: string[] = []; - - if (localRelayUrls.length === 0) { + try { + const workingRelays: string[] = []; + + if (localRelayUrls.length === 0) { + return workingRelays; + } + + // Test local relays quietly, without logging failures + await Promise.all( + localRelayUrls.map(async (url) => { + try { + const result = await testLocalRelayConnection(url, ndk); + if (result.connected) { + workingRelays.push(url); + console.debug(`[relay_management.ts] Local relay connected: ${url}`); + } + // Don't log failures - local relays are optional + } catch { + // Silently ignore local relay failures - they're optional + } + }) + ); + + if (workingRelays.length > 0) { + console.info(`[relay_management.ts] Found ${workingRelays.length} working local relays`); + } return workingRelays; + } catch { + // If anything goes wrong with the entire local relay testing process, + // just return an empty array silently + return []; } - - console.debug(`[relay_management.ts] Testing ${localRelayUrls.length} local relays...`); - - await Promise.all( - localRelayUrls.map(async (url) => { - try { - const result = await testRelayConnection(url, ndk); - if (result.connected) { - workingRelays.push(url); - console.debug(`[relay_management.ts] Local relay connected: ${url}`); - } else { - console.debug(`[relay_management.ts] Local relay failed: ${url} - ${result.error}`); - } - } catch { - // Silently ignore local relay failures - they're optional - console.debug(`[relay_management.ts] Local relay error (ignored): ${url}`); - } - }) - ); - - console.debug(`[relay_management.ts] Found ${workingRelays.length} working local relays`); - return workingRelays; } /** @@ -391,12 +527,18 @@ async function testRelaySet(relayUrls: string[], ndk: NDK): Promise { const workingRelays: string[] = []; const maxConcurrent = 2; // Reduce to 2 relays at a time to avoid overwhelming them + console.debug(`[relay_management.ts] Testing ${relayUrls.length} relays in batches of ${maxConcurrent}`); + console.debug(`[relay_management.ts] Relay URLs to test:`, relayUrls); + for (let i = 0; i < relayUrls.length; i += maxConcurrent) { const batch = relayUrls.slice(i, i + maxConcurrent); + console.debug(`[relay_management.ts] Testing batch ${Math.floor(i/maxConcurrent) + 1}:`, batch); const batchPromises = batch.map(async (url) => { try { + console.debug(`[relay_management.ts] Testing relay: ${url}`); const result = await testRelayConnection(url, ndk); + console.debug(`[relay_management.ts] Relay ${url} test result:`, result); return result.connected ? url : null; } catch (error) { console.debug(`[relay_management.ts] Failed to test relay ${url}:`, error); @@ -409,9 +551,12 @@ async function testRelaySet(relayUrls: string[], ndk: NDK): Promise { .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') .map(result => result.value) .filter((url): url is string => url !== null); + + console.debug(`[relay_management.ts] Batch ${Math.floor(i/maxConcurrent) + 1} working relays:`, batchWorkingRelays); workingRelays.push(...batchWorkingRelays); } + console.debug(`[relay_management.ts] Total working relays after testing:`, workingRelays); return workingRelays; } @@ -496,9 +641,16 @@ export async function buildCompleteRelaySet( }; } - // Use tested relays and deduplicate - const inboxRelays = testedInboxRelays.length > 0 ? deduplicateRelayUrls(testedInboxRelays) : deduplicateRelayUrls(secondaryRelays); - const outboxRelays = testedOutboxRelays.length > 0 ? deduplicateRelayUrls(testedOutboxRelays) : deduplicateRelayUrls(secondaryRelays); + // Always include some remote relays as fallback, even when local relays are working + const fallbackRelays = deduplicateRelayUrls([...anonymousRelays, ...secondaryRelays]); + + // Use tested relays and add fallback relays + const inboxRelays = testedInboxRelays.length > 0 + ? deduplicateRelayUrls([...testedInboxRelays, ...fallbackRelays]) + : deduplicateRelayUrls(fallbackRelays); + const outboxRelays = testedOutboxRelays.length > 0 + ? deduplicateRelayUrls([...testedOutboxRelays, ...fallbackRelays]) + : deduplicateRelayUrls(fallbackRelays); // Apply network condition optimization const currentNetworkCondition = get(networkCondition); @@ -515,8 +667,9 @@ export async function buildCompleteRelaySet( outboxRelays: deduplicateRelayUrls(networkOptimizedRelaySet.outboxRelays.filter((r: string) => !blockedRelays.includes(r))) }; - // If no relays are working, use anonymous relays as fallback + // Ensure we always have at least some relays if (finalRelaySet.inboxRelays.length === 0 && finalRelaySet.outboxRelays.length === 0) { + console.warn('[relay_management.ts] No relays available, using anonymous relays as final fallback'); return { inboxRelays: deduplicateRelayUrls(anonymousRelays), outboxRelays: deduplicateRelayUrls(anonymousRelays)