Browse Source

fixed relay connections

master
silberengel 7 months ago
parent
commit
43a7dcd14c
  1. 9
      src/lib/consts.ts
  2. 150
      src/lib/ndk.ts
  3. 4
      src/lib/stores/userStore.ts
  4. 263
      src/lib/utils/relay_management.ts

9
src/lib/consts.ts

@ -32,9 +32,7 @@ export const secondaryRelays = [
export const anonymousRelays = [ export const anonymousRelays = [
"wss://freelay.sovbit.host", "wss://freelay.sovbit.host",
"wss://thecitadel.nostr1.com", "wss://thecitadel.nostr1.com"
"wss://relay.damus.io",
"wss://relay.nostr.band"
]; ];
export const lowbandwidthRelays = [ export const lowbandwidthRelays = [
@ -44,8 +42,9 @@ export const lowbandwidthRelays = [
]; ];
export const localRelays: string[] = [ export const localRelays: string[] = [
"wss://localhost:8080", "ws://localhost:8080",
"wss://localhost:4869", "ws://localhost:4869",
"ws://localhost:3334"
]; ];
export enum FeedType { export enum FeedType {

150
src/lib/ndk.ts

@ -9,6 +9,7 @@ import NDK, {
import { writable, get, type Writable } from "svelte/store"; import { writable, get, type Writable } from "svelte/store";
import { import {
loginStorageKey, loginStorageKey,
anonymousRelays,
} from "./consts.ts"; } from "./consts.ts";
import { import {
buildCompleteRelaySet, buildCompleteRelaySet,
@ -91,8 +92,18 @@ function clearPersistentRelaySet(): void {
} }
// Subscribe to userStore changes and update ndkSignedIn accordingly // Subscribe to userStore changes and update ndkSignedIn accordingly
userStore.subscribe((userState) => { userStore.subscribe(async (userState) => {
ndkSignedIn.set(userState.signedIn); 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 * Ensures a relay URL uses secure WebSocket protocol
* @param url The relay URL to secure * @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 { 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://"); const secureUrl = url.replace(/^ws:\/\//, "wss://");
if (secureUrl !== url) { if (secureUrl !== url) {
console.warn( 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 * Creates a relay with proper authentication handling
*/ */
function createRelayWithAuth(url: string, ndk: NDK): NDKRelay { 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 // Ensure the URL is using appropriate protocol
const secureUrl = ensureSecureWebSocket(url); const secureUrl = ensureSecureWebSocket(url);
// Add connection timeout and error handling // Add connection timeout and error handling
const relay = new NDKRelay( const relay = new NDKRelay(
secureUrl, secureUrl,
NDKRelayAuthPolicies.signIn({ ndk }), NDKRelayAuthPolicies.signIn({ ndk }),
ndk, ndk,
); );
// Set up connection timeout // Set up connection timeout
const connectionTimeout = setTimeout(() => { const connectionTimeout = setTimeout(() => {
console.warn(`[NDK.ts] Connection timeout for ${secureUrl}`); try {
relay.disconnect(); console.warn(`[NDK.ts] Connection timeout for ${secureUrl}`);
}, 5000); // 5 second timeout relay.disconnect();
} catch {
// Set up custom authentication handling only if user is signed in // Silently ignore disconnect errors
if (ndk.signer && ndk.activeUser) { }
const authPolicy = new CustomRelayAuthPolicy(ndk); }, 5000); // 5 second timeout
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);
});
}
// Add error handling // Set up custom authentication handling only if user is signed in
relay.on("disconnect", () => { if (ndk.signer && ndk.activeUser) {
console.debug(`[NDK.ts] Relay disconnected: ${secureUrl}`); const authPolicy = new CustomRelayAuthPolicy(ndk);
clearTimeout(connectionTimeout); 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`); 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 * Updates relay stores when user state changes
* @param ndk NDK instance * @param ndk NDK instance
*/ */
export async function refreshRelayStores(ndk: NDK): Promise<void> { export async function refreshRelayStores(ndk: NDK): Promise<void> {
console.debug('[NDK.ts] Refreshing relay stores due to user state change'); 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
} }
/** /**

4
src/lib/stores/userStore.ts

@ -206,7 +206,7 @@ export async function loginWithExtension() {
// Update relay stores with the new user's relays // Update relay stores with the new user's relays
try { try {
console.debug('[userStore.ts] loginWithExtension: Updating relay stores for authenticated user'); 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) { } catch (error) {
console.warn('[userStore.ts] loginWithExtension: Failed to update relay stores:', 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 // Update relay stores with the new user's relays
try { try {
console.debug('[userStore.ts] loginWithAmber: Updating relay stores for authenticated user'); 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) { } catch (error) {
console.warn('[userStore.ts] loginWithAmber: Failed to update relay stores:', error); console.warn('[userStore.ts] loginWithAmber: Failed to update relay stores:', error);
} }

263
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 * Tests connection to a local relay (ws:// protocol)
* @param relayUrl The relay URL to test * @param relayUrl The local relay URL to test (should be ws://)
* @param ndk The NDK instance * @param ndk The NDK instance
* @returns Promise that resolves to connection status * @returns Promise that resolves to connection status
*/ */
export function testRelayConnection( export function testLocalRelayConnection(
relayUrl: string, relayUrl: string,
ndk: NDK, ndk: NDK,
): Promise<{ ): Promise<{
@ -58,8 +58,135 @@ export function testRelayConnection(
actualUrl?: string; actualUrl?: string;
}> { }> {
return new Promise((resolve) => { return new Promise((resolve) => {
// Ensure the URL is using wss:// protocol try {
const secureUrl = ensureSecureWebSocket(relayUrl); // 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 // Use the existing NDK instance instead of creating a new one
const relay = new NDKRelay(secureUrl, undefined, ndk); const relay = new NDKRelay(secureUrl, undefined, ndk);
@ -69,6 +196,7 @@ export function testRelayConnection(
let actualUrl: string | undefined; let actualUrl: string | undefined;
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
console.debug(`[relay_management.ts] Relay ${secureUrl} connection timeout`);
relay.disconnect(); relay.disconnect();
resolve({ resolve({
connected: false, connected: false,
@ -76,9 +204,10 @@ export function testRelayConnection(
error: "Connection timeout", error: "Connection timeout",
actualUrl, actualUrl,
}); });
}, 3000); // Increased timeout to 3 seconds to give relays more time }, 3000);
relay.on("connect", () => { relay.on("connect", () => {
console.debug(`[relay_management.ts] Relay ${secureUrl} connected successfully`);
connected = true; connected = true;
actualUrl = secureUrl; actualUrl = secureUrl;
clearTimeout(timeout); clearTimeout(timeout);
@ -99,6 +228,7 @@ export function testRelayConnection(
relay.on("disconnect", () => { relay.on("disconnect", () => {
if (!connected) { if (!connected) {
console.debug(`[relay_management.ts] Relay ${secureUrl} disconnected without connecting`);
error = "Connection failed"; error = "Connection failed";
clearTimeout(timeout); clearTimeout(timeout);
resolve({ resolve({
@ -113,30 +243,31 @@ export function testRelayConnection(
relay.connect(); relay.connect();
}); });
} }
/** /**
* Ensures a relay URL uses secure WebSocket protocol for remote relays * Tests connection to a relay and returns connection status
* @param url The relay URL to secure * @param relayUrl The relay URL to test
* @returns The URL with wss:// protocol (except for localhost) * @param ndk The NDK instance
* @returns Promise that resolves to connection status
*/ */
function ensureSecureWebSocket(url: string): string { export function testRelayConnection(
// For localhost, always use ws:// (never wss://) relayUrl: string,
if (url.includes('localhost') || url.includes('127.0.0.1')) { ndk: NDK,
// Convert any wss://localhost to ws://localhost ): Promise<{
return url.replace(/^wss:\/\//, "ws://"); connected: boolean;
} requiresAuth: boolean;
error?: string;
// Replace ws:// with wss:// for remote relays actualUrl?: string;
const secureUrl = url.replace(/^ws:\/\//, "wss://"); }> {
// Determine if this is a local or remote relay
if (secureUrl !== url) { if (relayUrl.includes('localhost') || relayUrl.includes('127.0.0.1')) {
console.warn( return testLocalRelayConnection(relayUrl, ndk);
`[relay_management.ts] Protocol upgrade for rem ote relay: ${url} -> ${secureUrl}`, } else {
); return testRemoteRelayConnection(relayUrl, ndk);
} }
return secureUrl;
} }
/** /**
* Tests connection to local relays * 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 * @returns Promise that resolves to array of working local relay URLs
*/ */
async function testLocalRelays(localRelayUrls: string[], ndk: NDK): Promise<string[]> { async function testLocalRelays(localRelayUrls: string[], ndk: NDK): Promise<string[]> {
const workingRelays: string[] = []; try {
const workingRelays: string[] = [];
if (localRelayUrls.length === 0) {
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; 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<string[]> {
const workingRelays: string[] = []; const workingRelays: string[] = [];
const maxConcurrent = 2; // Reduce to 2 relays at a time to avoid overwhelming them 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) { for (let i = 0; i < relayUrls.length; i += maxConcurrent) {
const batch = relayUrls.slice(i, 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) => { const batchPromises = batch.map(async (url) => {
try { try {
console.debug(`[relay_management.ts] Testing relay: ${url}`);
const result = await testRelayConnection(url, ndk); const result = await testRelayConnection(url, ndk);
console.debug(`[relay_management.ts] Relay ${url} test result:`, result);
return result.connected ? url : null; return result.connected ? url : null;
} catch (error) { } catch (error) {
console.debug(`[relay_management.ts] Failed to test relay ${url}:`, error); console.debug(`[relay_management.ts] Failed to test relay ${url}:`, error);
@ -409,9 +551,12 @@ async function testRelaySet(relayUrls: string[], ndk: NDK): Promise<string[]> {
.filter((result): result is PromiseFulfilledResult<string | null> => result.status === 'fulfilled') .filter((result): result is PromiseFulfilledResult<string | null> => result.status === 'fulfilled')
.map(result => result.value) .map(result => result.value)
.filter((url): url is string => url !== null); .filter((url): url is string => url !== null);
console.debug(`[relay_management.ts] Batch ${Math.floor(i/maxConcurrent) + 1} working relays:`, batchWorkingRelays);
workingRelays.push(...batchWorkingRelays); workingRelays.push(...batchWorkingRelays);
} }
console.debug(`[relay_management.ts] Total working relays after testing:`, workingRelays);
return workingRelays; return workingRelays;
} }
@ -496,9 +641,16 @@ export async function buildCompleteRelaySet(
}; };
} }
// Use tested relays and deduplicate // Always include some remote relays as fallback, even when local relays are working
const inboxRelays = testedInboxRelays.length > 0 ? deduplicateRelayUrls(testedInboxRelays) : deduplicateRelayUrls(secondaryRelays); const fallbackRelays = deduplicateRelayUrls([...anonymousRelays, ...secondaryRelays]);
const outboxRelays = testedOutboxRelays.length > 0 ? deduplicateRelayUrls(testedOutboxRelays) : deduplicateRelayUrls(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 // Apply network condition optimization
const currentNetworkCondition = get(networkCondition); const currentNetworkCondition = get(networkCondition);
@ -515,8 +667,9 @@ export async function buildCompleteRelaySet(
outboxRelays: deduplicateRelayUrls(networkOptimizedRelaySet.outboxRelays.filter((r: string) => !blockedRelays.includes(r))) 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) { if (finalRelaySet.inboxRelays.length === 0 && finalRelaySet.outboxRelays.length === 0) {
console.warn('[relay_management.ts] No relays available, using anonymous relays as final fallback');
return { return {
inboxRelays: deduplicateRelayUrls(anonymousRelays), inboxRelays: deduplicateRelayUrls(anonymousRelays),
outboxRelays: deduplicateRelayUrls(anonymousRelays) outboxRelays: deduplicateRelayUrls(anonymousRelays)

Loading…
Cancel
Save