125 lines
4.3 KiB
TypeScript
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
|
|
)
|
|
}
|