From a19990fff67d83fc579d1558f991a12a5c4a3faa Mon Sep 17 00:00:00 2001 From: Silberengel Date: Tue, 11 Nov 2025 07:25:43 +0100 Subject: [PATCH] handle nostr.band api outage gracefully --- src/components/TrendingNotes/index.tsx | 67 ++++++++++++++++++++------ src/i18n/locales/en.ts | 1 + src/services/client.service.ts | 25 +++++++++- 3 files changed, 78 insertions(+), 15 deletions(-) diff --git a/src/components/TrendingNotes/index.tsx b/src/components/TrendingNotes/index.tsx index e06b2f5..8c6d380 100644 --- a/src/components/TrendingNotes/index.tsx +++ b/src/components/TrendingNotes/index.tsx @@ -40,6 +40,7 @@ export default function TrendingNotes() { const { zapReplyThreshold } = useZap() const [nostrEvents, setNostrEvents] = useState([]) const [nostrLoading, setNostrLoading] = useState(false) + const [nostrError, setNostrError] = useState(null) const [showCount, setShowCount] = useState(SHOW_COUNT) const [activeTab, setActiveTab] = useState('nostr') const [sortOrder, setSortOrder] = useState('most-popular') @@ -49,25 +50,48 @@ export default function TrendingNotes() { const [cacheEvents, setCacheEvents] = useState([]) const [cacheLoading, setCacheLoading] = useState(false) const bottomRef = useRef(null) + const isFetchingNostrRef = useRef(false) // Load Nostr.band trending feed when tab is active useEffect(() => { const loadTrending = async () => { + // Prevent concurrent fetches + if (isFetchingNostrRef.current) { + return + } + try { + isFetchingNostrRef.current = true setNostrLoading(true) + setNostrError(null) const events = await client.fetchTrendingNotes() setNostrEvents(events) + setNostrError(null) } catch (error) { - logger.warn('Failed to load nostr.band trending notes', error as Error) + if (error instanceof Error && error.message === 'TIMEOUT') { + setNostrError('timeout') + logger.warn('nostr.band API request timed out after 5 seconds') + } else { + logger.warn('Failed to load nostr.band trending notes', error as Error) + setNostrError(null) // Other errors are handled silently (empty array) + } } finally { setNostrLoading(false) + isFetchingNostrRef.current = false } } - if (activeTab === 'nostr' && nostrEvents.length === 0 && !nostrLoading) { + if (activeTab === 'nostr' && nostrEvents.length === 0 && !nostrLoading && !nostrError && !isFetchingNostrRef.current) { loadTrending() } - }, [activeTab, nostrEvents.length, nostrLoading]) + }, [activeTab, nostrEvents.length, nostrLoading, nostrError]) + + // Reset error when switching away from nostr tab + useEffect(() => { + if (activeTab !== 'nostr') { + setNostrError(null) + } + }, [activeTab]) // Debug: Track cacheEvents changes useEffect(() => { @@ -562,34 +586,34 @@ export default function TrendingNotes() { Trending:
@@ -675,8 +699,15 @@ export default function TrendingNotes() { )} - {/* Show loading message for nostr tab */} - {activeTab === 'nostr' && nostrLoading && nostrEvents.length === 0 && ( + {/* Show error message for nostr tab timeout (show instead of loading when error occurs, only if no events) */} + {activeTab === 'nostr' && nostrError === 'timeout' && !nostrLoading && filteredEvents.length === 0 && ( +
+ {t('The nostr.band relay appears to be temporarily out of service. Please try again later.')} +
+ )} + + {/* Show loading message for nostr tab (only if not in error state) */} + {activeTab === 'nostr' && nostrLoading && nostrEvents.length === 0 && !nostrError && (
Loading trending notes from nostr.band...
@@ -691,6 +722,14 @@ export default function TrendingNotes() { {filteredEvents.map((event) => ( ))} + + {/* Show error message at the end for nostr tab timeout (only if there are events) */} + {activeTab === 'nostr' && nostrError === 'timeout' && !nostrLoading && filteredEvents.length > 0 && ( +
+ {t('The nostr.band relay appears to be temporarily out of service. Please try again later.')} +
+ )} + {(() => { const totalAvailableLength = activeTab === 'nostr' diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 1e40a9c..cc772e3 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -30,6 +30,7 @@ export default { 'loading...': 'loading...', 'Loading...': 'Loading...', 'no more notes': 'no more notes', + 'The nostr.band relay appears to be temporarily out of service. Please try again later.': 'The nostr.band relay appears to be temporarily out of service. Please try again later.', 'reply to': 'reply to', reply: 'reply', Reply: 'Reply', diff --git a/src/services/client.service.ts b/src/services/client.service.ts index 451bf36..2a1ff6c 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -873,7 +873,23 @@ class ClientService extends EventTarget { } try { - const response = await fetch('https://api.nostr.band/v0/trending/notes') + // Create a timeout promise that rejects after 5 seconds + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + reject(new Error('TIMEOUT')) + }, 5000) + }) + + // Race between the fetch and timeout + const response = await Promise.race([ + fetch('https://api.nostr.band/v0/trending/notes'), + timeoutPromise + ]) + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`) + } + const data = await response.json() const events: NEvent[] = [] for (const note of data.notes ?? []) { @@ -895,6 +911,13 @@ class ClientService extends EventTarget { this.trendingNotesCache = events return this.trendingNotesCache } catch (error) { + // Re-throw timeout errors so the component can handle them + // Don't cache on timeout - let the component handle the error state + if (error instanceof Error && error.message === 'TIMEOUT') { + throw error + } + // For other errors, return empty array and cache it (existing behavior) + this.trendingNotesCache = [] return [] } }