/** * Relay rotation utility * Tries relays in sequence, rotating through the list on failure * No retry on individual relay, just move to next and loop * Relays that fail are marked inactive for the session */ import type { SimplePool } from 'nostr-tools' import type { Filter } from 'nostr-tools' import type { SimplePoolWithSub } from '@/types/nostr-tools-extended' import { createSubscription } from '@/types/nostr-tools-extended' import { relaySessionManager } from './relaySessionManager' /** * Try to execute an operation with relay rotation * Tries each relay in sequence, moving to next on failure * Loops back to first relay after trying all */ export async function tryWithRelayRotation( pool: SimplePool, operation: (relayUrl: string, pool: SimplePoolWithSub) => Promise, timeout: number = 10000 ): Promise { // Get active relays (enabled and not marked inactive for this session) const activeRelays = await relaySessionManager.getActiveRelays() if (activeRelays.length === 0) { throw new Error('No active relays available') } let lastError: Error | null = null let attempts = 0 const maxAttempts = activeRelays.length * 2 // Try all active relays twice (loop once) while (attempts < maxAttempts) { // Get current active relays (may have changed if some were marked inactive) const currentActiveRelays = await relaySessionManager.getActiveRelays() if (currentActiveRelays.length === 0) { throw new Error('No active relays available') } const relayIndex = attempts % currentActiveRelays.length const relayUrl = currentActiveRelays[relayIndex] if (!relayUrl) { throw new Error('Invalid relay configuration') } // Skip if relay was marked failed during the loop (it will be at the bottom now) // We continue to use it but it's lower priority try { console.log(`[RelayRotation] Trying relay ${relayIndex + 1}/${currentActiveRelays.length}: ${relayUrl}`) // Notify progress manager that we're switching to a new relay (reset to 0 for this relay) const { syncProgressManager } = await import('./syncProgressManager') const currentProgress = syncProgressManager.getProgress() if (currentProgress) { syncProgressManager.setProgress({ ...currentProgress, currentStep: 0, // Reset to 0 when changing relay currentRelay: relayUrl, }) } const poolWithSub = pool as unknown as SimplePoolWithSub const result = await Promise.race([ operation(relayUrl, poolWithSub), new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout) ), ]) console.log(`[RelayRotation] Success with relay: ${relayUrl}`) return result } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) console.warn(`[RelayRotation] Relay ${relayUrl} failed: ${errorMessage}`) // Mark relay as failed (move to bottom of priority list) relaySessionManager.markRelayFailed(relayUrl) lastError = error instanceof Error ? error : new Error(String(error)) attempts++ // If we've tried all relays once, loop back if (attempts < maxAttempts) { continue } } } // If we get here, all relays failed throw lastError ?? new Error('All relays failed') } /** * Create a subscription with relay rotation * Tries each relay until one succeeds */ export async function createSubscriptionWithRotation( pool: SimplePool, filters: Filter[], timeout: number = 10000 ): Promise<{ subscription: import('@/types/nostr-tools-extended').Subscription relayUrl: string }> { return tryWithRelayRotation( pool, async (relayUrl, poolWithSub) => { const subscription = createSubscription(poolWithSub, [relayUrl], filters) return { subscription, relayUrl } }, timeout ) }