story-research-zapwall/lib/relayRotation.ts
2026-01-08 21:49:57 +01:00

117 lines
3.9 KiB
TypeScript

/**
* 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<T>(
pool: SimplePool,
operation: (relayUrl: string, pool: SimplePoolWithSub) => Promise<T>,
timeout: number = 10000
): Promise<T> {
// 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.warn(`[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<never>((_, reject) =>
setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout)
),
])
console.warn(`[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) {
break
}
}
}
// 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
)
}