story-research-zapwall/lib/relayRotation.ts
2026-01-08 23:53:05 +01:00

125 lines
4.3 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> {
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<T> = { ok: true; value: T } | { ok: false; error: Error }
async function tryOperationOnRelay<T>(params: {
relayUrl: string
relayIndex: number
totalRelays: number
poolWithSub: SimplePoolWithSub
operation: (relayUrl: string, pool: SimplePoolWithSub) => Promise<T>
timeout: number
}): Promise<RelayAttemptResult<T>> {
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<void> {
const { syncProgressManager } = await import('./syncProgressManager')
const currentProgress = syncProgressManager.getProgress()
if (!currentProgress) {
return
}
syncProgressManager.setProgress({ ...currentProgress, currentStep: 0, currentRelay: relayUrl })
}
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
return Promise.race([
promise,
new Promise<never>((_, 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
)
}