/** * 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 { const initialActiveRelays = await relaySessionManager.getActiveRelays() if (initialActiveRelays.length === 0) { throw new Error('No active relays available') } const maxAttempts = initialActiveRelays.length * 2 // Try all active relays twice (loop once) const poolWithSub = pool as unknown as SimplePoolWithSub let lastError: Error | null = null for (let attempts = 0; attempts < maxAttempts; attempts += 1) { const { relayUrl, relayIndex, totalRelays } = await pickRelayForAttempt(attempts) const attempt = await tryOperationOnRelay({ relayUrl, relayIndex, totalRelays, poolWithSub, operation, timeout }) if (attempt.ok) { return attempt.value } lastError = attempt.error } throw lastError ?? new Error('All relays failed') } async function pickRelayForAttempt(attempts: number): Promise<{ relayUrl: string; relayIndex: number; totalRelays: number }> { 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') } return { relayUrl, relayIndex, totalRelays: currentActiveRelays.length } } type RelayAttemptResult = { ok: true; value: T } | { ok: false; error: Error } async function tryOperationOnRelay(params: { relayUrl: string relayIndex: number totalRelays: number poolWithSub: SimplePoolWithSub operation: (relayUrl: string, pool: SimplePoolWithSub) => Promise timeout: number }): Promise> { console.warn(`[RelayRotation] Trying relay ${params.relayIndex + 1}/${params.totalRelays}: ${params.relayUrl}`) await updateSyncProgressForRelay(params.relayUrl) try { const value = await withTimeout(params.operation(params.relayUrl, params.poolWithSub), params.timeout) console.warn(`[RelayRotation] Success with relay: ${params.relayUrl}`) return { ok: true, value } } catch (error) { const err = error instanceof Error ? error : new Error(String(error)) console.warn(`[RelayRotation] Relay ${params.relayUrl} failed: ${err.message}`) relaySessionManager.markRelayFailed(params.relayUrl) return { ok: false, error: err } } } async function updateSyncProgressForRelay(relayUrl: string): Promise { const { syncProgressManager } = await import('./syncProgressManager') const currentProgress = syncProgressManager.getProgress() if (!currentProgress) { return } syncProgressManager.setProgress({ ...currentProgress, currentStep: 0, currentRelay: relayUrl }) } async function withTimeout(promise: Promise, timeoutMs: number): Promise { return Promise.race([ promise, new Promise((_, reject) => { setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms`)), timeoutMs) }), ]) } /** * 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 ) }