Browse Source

handle nostr.band api outage gracefully

imwald
Silberengel 4 months ago
parent
commit
a19990fff6
  1. 67
      src/components/TrendingNotes/index.tsx
  2. 1
      src/i18n/locales/en.ts
  3. 25
      src/services/client.service.ts

67
src/components/TrendingNotes/index.tsx

@ -40,6 +40,7 @@ export default function TrendingNotes() {
const { zapReplyThreshold } = useZap() const { zapReplyThreshold } = useZap()
const [nostrEvents, setNostrEvents] = useState<NostrEvent[]>([]) const [nostrEvents, setNostrEvents] = useState<NostrEvent[]>([])
const [nostrLoading, setNostrLoading] = useState(false) const [nostrLoading, setNostrLoading] = useState(false)
const [nostrError, setNostrError] = useState<string | null>(null)
const [showCount, setShowCount] = useState(SHOW_COUNT) const [showCount, setShowCount] = useState(SHOW_COUNT)
const [activeTab, setActiveTab] = useState<TrendingTab>('nostr') const [activeTab, setActiveTab] = useState<TrendingTab>('nostr')
const [sortOrder, setSortOrder] = useState<SortOrder>('most-popular') const [sortOrder, setSortOrder] = useState<SortOrder>('most-popular')
@ -49,25 +50,48 @@ export default function TrendingNotes() {
const [cacheEvents, setCacheEvents] = useState<NostrEvent[]>([]) const [cacheEvents, setCacheEvents] = useState<NostrEvent[]>([])
const [cacheLoading, setCacheLoading] = useState(false) const [cacheLoading, setCacheLoading] = useState(false)
const bottomRef = useRef<HTMLDivElement>(null) const bottomRef = useRef<HTMLDivElement>(null)
const isFetchingNostrRef = useRef(false)
// Load Nostr.band trending feed when tab is active // Load Nostr.band trending feed when tab is active
useEffect(() => { useEffect(() => {
const loadTrending = async () => { const loadTrending = async () => {
// Prevent concurrent fetches
if (isFetchingNostrRef.current) {
return
}
try { try {
isFetchingNostrRef.current = true
setNostrLoading(true) setNostrLoading(true)
setNostrError(null)
const events = await client.fetchTrendingNotes() const events = await client.fetchTrendingNotes()
setNostrEvents(events) setNostrEvents(events)
setNostrError(null)
} catch (error) { } 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 { } finally {
setNostrLoading(false) setNostrLoading(false)
isFetchingNostrRef.current = false
} }
} }
if (activeTab === 'nostr' && nostrEvents.length === 0 && !nostrLoading) { if (activeTab === 'nostr' && nostrEvents.length === 0 && !nostrLoading && !nostrError && !isFetchingNostrRef.current) {
loadTrending() 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 // Debug: Track cacheEvents changes
useEffect(() => { useEffect(() => {
@ -562,34 +586,34 @@ export default function TrendingNotes() {
<span className="text-sm font-medium text-muted-foreground">Trending:</span> <span className="text-sm font-medium text-muted-foreground">Trending:</span>
<div className="flex gap-1"> <div className="flex gap-1">
<button <button
onClick={() => setActiveTab('nostr')} onClick={() => setActiveTab('relays')}
className={`px-3 py-1 text-sm rounded-md transition-colors ${ className={`px-3 py-1 text-sm rounded-md transition-colors ${
activeTab === 'nostr' activeTab === 'relays'
? 'bg-primary text-primary-foreground' ? 'bg-primary text-primary-foreground'
: 'bg-muted hover:bg-muted/80 text-muted-foreground' : 'bg-muted hover:bg-muted/80 text-muted-foreground'
}`} }`}
> >
on Nostr on your relays
</button> </button>
<button <button
onClick={() => setActiveTab('relays')} onClick={() => setActiveTab('hashtags')}
className={`px-3 py-1 text-sm rounded-md transition-colors ${ className={`px-3 py-1 text-sm rounded-md transition-colors ${
activeTab === 'relays' activeTab === 'hashtags'
? 'bg-primary text-primary-foreground' ? 'bg-primary text-primary-foreground'
: 'bg-muted hover:bg-muted/80 text-muted-foreground' : 'bg-muted hover:bg-muted/80 text-muted-foreground'
}`} }`}
> >
on your relays hashtags
</button> </button>
<button <button
onClick={() => setActiveTab('hashtags')} onClick={() => setActiveTab('nostr')}
className={`px-3 py-1 text-sm rounded-md transition-colors ${ className={`px-3 py-1 text-sm rounded-md transition-colors ${
activeTab === 'hashtags' activeTab === 'nostr'
? 'bg-primary text-primary-foreground' ? 'bg-primary text-primary-foreground'
: 'bg-muted hover:bg-muted/80 text-muted-foreground' : 'bg-muted hover:bg-muted/80 text-muted-foreground'
}`} }`}
> >
hashtags on Nostr
</button> </button>
</div> </div>
</div> </div>
@ -675,8 +699,15 @@ export default function TrendingNotes() {
)} )}
</div> </div>
{/* Show loading message for nostr tab */} {/* Show error message for nostr tab timeout (show instead of loading when error occurs, only if no events) */}
{activeTab === 'nostr' && nostrLoading && nostrEvents.length === 0 && ( {activeTab === 'nostr' && nostrError === 'timeout' && !nostrLoading && filteredEvents.length === 0 && (
<div className="text-center text-sm text-muted-foreground mt-8 px-4 py-2 bg-muted/50 rounded-md mx-4">
{t('The nostr.band relay appears to be temporarily out of service. Please try again later.')}
</div>
)}
{/* Show loading message for nostr tab (only if not in error state) */}
{activeTab === 'nostr' && nostrLoading && nostrEvents.length === 0 && !nostrError && (
<div className="text-center text-sm text-muted-foreground mt-8"> <div className="text-center text-sm text-muted-foreground mt-8">
Loading trending notes from nostr.band... Loading trending notes from nostr.band...
</div> </div>
@ -691,6 +722,14 @@ export default function TrendingNotes() {
{filteredEvents.map((event) => ( {filteredEvents.map((event) => (
<NoteCard key={event.id} className="w-full" event={event} /> <NoteCard key={event.id} className="w-full" event={event} />
))} ))}
{/* Show error message at the end for nostr tab timeout (only if there are events) */}
{activeTab === 'nostr' && nostrError === 'timeout' && !nostrLoading && filteredEvents.length > 0 && (
<div className="text-center text-sm text-muted-foreground mt-4 px-4 py-2 bg-muted/50 rounded-md mx-4">
{t('The nostr.band relay appears to be temporarily out of service. Please try again later.')}
</div>
)}
{(() => { {(() => {
const totalAvailableLength = const totalAvailableLength =
activeTab === 'nostr' activeTab === 'nostr'

1
src/i18n/locales/en.ts

@ -30,6 +30,7 @@ export default {
'loading...': 'loading...', 'loading...': 'loading...',
'Loading...': 'Loading...', 'Loading...': 'Loading...',
'no more notes': 'no more notes', '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 to': 'reply to',
reply: 'reply', reply: 'reply',
Reply: 'Reply', Reply: 'Reply',

25
src/services/client.service.ts

@ -873,7 +873,23 @@ class ClientService extends EventTarget {
} }
try { 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<never>((_, 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 data = await response.json()
const events: NEvent[] = [] const events: NEvent[] = []
for (const note of data.notes ?? []) { for (const note of data.notes ?? []) {
@ -895,6 +911,13 @@ class ClientService extends EventTarget {
this.trendingNotesCache = events this.trendingNotesCache = events
return this.trendingNotesCache return this.trendingNotesCache
} catch (error) { } 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 [] return []
} }
} }

Loading…
Cancel
Save