import { FETCH_RELAY_LIST_HOOK_MAX_MS } from '@/constants' import logger from '@/lib/logger' import client from '@/services/client.service' import indexedDb from '@/services/indexed-db.service' import { TRelayList } from '@/types' import { kinds } from 'nostr-tools' import { useEffect, useState } from 'react' const emptyRelayList = (): TRelayList => ({ write: [], read: [], originalRelays: [], httpRead: [], httpWrite: [], httpOriginalRelays: [] }) export function useFetchRelayList(pubkey?: string | null) { const [relayList, setRelayList] = useState(emptyRelayList) const [isFetching, setIsFetching] = useState(true) /** True when IndexedDB has this author's kind 10002 (even if `originalRelays` is empty after merge). */ const [hasKind10002InStorage, setHasKind10002InStorage] = useState(false) useEffect(() => { let cancelled = false const targetPk = pubkey?.trim() || null const fetchRelayList = async () => { setIsFetching(true) setHasKind10002InStorage(false) if (!targetPk) { setRelayList(emptyRelayList()) setIsFetching(false) return } setRelayList(emptyRelayList()) try { const [fromStorage, k10002] = await Promise.all([ client.peekRelayListFromStorage(targetPk), indexedDb.getReplaceableEvent(targetPk, kinds.RelayList).catch(() => null) ]) if (cancelled) return setHasKind10002InStorage(!!k10002) setRelayList(fromStorage) const merged = await Promise.race([ client.fetchRelayList(targetPk), new Promise((_, reject) => { setTimeout(() => reject(new Error('relay-list hook max wait')), FETCH_RELAY_LIST_HOOK_MAX_MS) }) ]).catch(async (err: unknown) => { const isMaxWait = err instanceof Error && err.message === 'relay-list hook max wait' if (isMaxWait) { logger.warn('[useFetchRelayList] fetchRelayList exceeded max wait; clearing dedupe cache', { pubkeyPrefix: targetPk.slice(0, 12) }) client.clearRelayListCache(targetPk) return client.peekRelayListFromStorage(targetPk) } throw err }) if (cancelled) return setRelayList(merged) const k10002After = await indexedDb.getReplaceableEvent(targetPk, kinds.RelayList).catch(() => null) if (!cancelled) { setHasKind10002InStorage(!!k10002After) } } catch (err) { logger.error('Failed to fetch relay list', { error: err, pubkey: targetPk }) try { const fallback = await client.peekRelayListFromStorage(targetPk) const k10002 = await indexedDb.getReplaceableEvent(targetPk, kinds.RelayList).catch(() => null) if (!cancelled) { setRelayList(fallback) setHasKind10002InStorage(!!k10002) } } catch { if (!cancelled) { setRelayList(emptyRelayList()) } } } finally { if (!cancelled) { setIsFetching(false) } } } void fetchRelayList() return () => { cancelled = true } }, [pubkey]) /** True when no kind 10002 for this author in IDB — UI may show default discovery relays with a disclaimer. */ const showingRelayListFallback = !isFetching && !hasKind10002InStorage return { relayList, isFetching, hasKind10002InStorage, showingRelayListFallback } }