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() { @@ -40,6 +40,7 @@ export default function TrendingNotes() {
const { zapReplyThreshold } = useZap()
const [nostrEvents, setNostrEvents] = useState<NostrEvent[]>([])
const [nostrLoading, setNostrLoading] = useState(false)
const [nostrError, setNostrError] = useState<string | null>(null)
const [showCount, setShowCount] = useState(SHOW_COUNT)
const [activeTab, setActiveTab] = useState<TrendingTab>('nostr')
const [sortOrder, setSortOrder] = useState<SortOrder>('most-popular')
@ -49,25 +50,48 @@ export default function TrendingNotes() { @@ -49,25 +50,48 @@ export default function TrendingNotes() {
const [cacheEvents, setCacheEvents] = useState<NostrEvent[]>([])
const [cacheLoading, setCacheLoading] = useState(false)
const bottomRef = useRef<HTMLDivElement>(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() { @@ -562,34 +586,34 @@ export default function TrendingNotes() {
<span className="text-sm font-medium text-muted-foreground">Trending:</span>
<div className="flex gap-1">
<button
onClick={() => setActiveTab('nostr')}
onClick={() => setActiveTab('relays')}
className={`px-3 py-1 text-sm rounded-md transition-colors ${
activeTab === 'nostr'
activeTab === 'relays'
? 'bg-primary text-primary-foreground'
: 'bg-muted hover:bg-muted/80 text-muted-foreground'
}`}
>
on Nostr
on your relays
</button>
<button
onClick={() => setActiveTab('relays')}
onClick={() => setActiveTab('hashtags')}
className={`px-3 py-1 text-sm rounded-md transition-colors ${
activeTab === 'relays'
activeTab === 'hashtags'
? 'bg-primary text-primary-foreground'
: 'bg-muted hover:bg-muted/80 text-muted-foreground'
}`}
>
on your relays
hashtags
</button>
<button
onClick={() => setActiveTab('hashtags')}
onClick={() => setActiveTab('nostr')}
className={`px-3 py-1 text-sm rounded-md transition-colors ${
activeTab === 'hashtags'
activeTab === 'nostr'
? 'bg-primary text-primary-foreground'
: 'bg-muted hover:bg-muted/80 text-muted-foreground'
}`}
>
hashtags
on Nostr
</button>
</div>
</div>
@ -675,8 +699,15 @@ export default function TrendingNotes() { @@ -675,8 +699,15 @@ export default function TrendingNotes() {
)}
</div>
{/* 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 && (
<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">
Loading trending notes from nostr.band...
</div>
@ -691,6 +722,14 @@ export default function TrendingNotes() { @@ -691,6 +722,14 @@ export default function TrendingNotes() {
{filteredEvents.map((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 =
activeTab === 'nostr'

1
src/i18n/locales/en.ts

@ -30,6 +30,7 @@ export default { @@ -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',

25
src/services/client.service.ts

@ -873,7 +873,23 @@ class ClientService extends EventTarget { @@ -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<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 events: NEvent[] = []
for (const note of data.notes ?? []) {
@ -895,6 +911,13 @@ class ClientService extends EventTarget { @@ -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 []
}
}

Loading…
Cancel
Save