Fix event detail 404 by retrying instead of caching empty results
The NPool's 500ms eoseTimeout causes event-by-ID queries to resolve before slower relays respond. Previously, useEvent returned null on the first miss and the page immediately showed a 404. Now: - useEvent/useAddrEvent throw on miss so TanStack Query retries (3 retries with exponential backoff: 2s, 4s, 8s) - PostDetailPage shows the skeleton while retrying (checks isFetching) - Only shows 404 after all retries are exhausted - Feed cache seeding from previous commit still provides instant navigation for events already seen in the feed Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
+25
-18
@@ -2,7 +2,7 @@ import { useNostr } from '@nostrify/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { NostrEvent } from '@nostrify/nostrify';
|
||||
|
||||
/** Fetches a single Nostr event by its hex ID, optionally querying relay hints. */
|
||||
/** Fetches a single Nostr event by its hex ID, with retries to handle slow relays. */
|
||||
export function useEvent(eventId: string | undefined, relays?: string[]) {
|
||||
const { nostr } = useNostr();
|
||||
|
||||
@@ -10,28 +10,33 @@ export function useEvent(eventId: string | undefined, relays?: string[]) {
|
||||
queryKey: ['event', eventId ?? ''],
|
||||
queryFn: async ({ signal }) => {
|
||||
if (!eventId) return null;
|
||||
const querySignal = AbortSignal.any([signal, AbortSignal.timeout(5000)]);
|
||||
const filter = [{ ids: [eventId], limit: 1 }];
|
||||
|
||||
// Query the user's configured relays first
|
||||
const events = await nostr.query(filter, { signal: querySignal });
|
||||
// Query the user's configured relays
|
||||
const events = await nostr.query(filter, {
|
||||
signal: AbortSignal.any([signal, AbortSignal.timeout(8000)]),
|
||||
});
|
||||
if (events.length > 0) return events[0];
|
||||
|
||||
// If not found and we have relay hints, try those relays directly
|
||||
// Try relay hints if available
|
||||
if (relays && relays.length > 0) {
|
||||
try {
|
||||
const hintSignal = AbortSignal.any([signal, AbortSignal.timeout(5000)]);
|
||||
const hintEvents = await nostr.group(relays).query(filter, { signal: hintSignal });
|
||||
const hintEvents = await nostr.group(relays).query(filter, {
|
||||
signal: AbortSignal.any([signal, AbortSignal.timeout(8000)]),
|
||||
});
|
||||
if (hintEvents.length > 0) return hintEvents[0];
|
||||
} catch {
|
||||
// relay hint query failed — fall through
|
||||
// relay hint query failed
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
// Throw so TanStack Query retries instead of caching null
|
||||
throw new Error('Event not found');
|
||||
},
|
||||
enabled: !!eventId,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
retry: 3,
|
||||
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 8000),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -42,7 +47,7 @@ export interface AddrCoords {
|
||||
identifier: string;
|
||||
}
|
||||
|
||||
/** Fetches a single addressable Nostr event by kind + pubkey + d-tag, optionally querying relay hints. */
|
||||
/** Fetches a single addressable Nostr event by kind + pubkey + d-tag, with retries. */
|
||||
export function useAddrEvent(addr: AddrCoords | undefined, relays?: string[]) {
|
||||
const { nostr } = useNostr();
|
||||
|
||||
@@ -50,27 +55,29 @@ export function useAddrEvent(addr: AddrCoords | undefined, relays?: string[]) {
|
||||
queryKey: ['addr-event', addr?.kind ?? 0, addr?.pubkey ?? '', addr?.identifier ?? ''],
|
||||
queryFn: async ({ signal }) => {
|
||||
if (!addr) return null;
|
||||
const querySignal = AbortSignal.any([signal, AbortSignal.timeout(5000)]);
|
||||
const filter = [{ kinds: [addr.kind], authors: [addr.pubkey], '#d': [addr.identifier], limit: 1 }];
|
||||
|
||||
// Query the user's configured relays first
|
||||
const events = await nostr.query(filter, { signal: querySignal });
|
||||
const events = await nostr.query(filter, {
|
||||
signal: AbortSignal.any([signal, AbortSignal.timeout(8000)]),
|
||||
});
|
||||
if (events.length > 0) return events[0];
|
||||
|
||||
// If not found and we have relay hints, try those relays directly
|
||||
if (relays && relays.length > 0) {
|
||||
try {
|
||||
const hintSignal = AbortSignal.any([signal, AbortSignal.timeout(5000)]);
|
||||
const hintEvents = await nostr.group(relays).query(filter, { signal: hintSignal });
|
||||
const hintEvents = await nostr.group(relays).query(filter, {
|
||||
signal: AbortSignal.any([signal, AbortSignal.timeout(8000)]),
|
||||
});
|
||||
if (hintEvents.length > 0) return hintEvents[0];
|
||||
} catch {
|
||||
// relay hint query failed — fall through
|
||||
// relay hint query failed
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
throw new Error('Event not found');
|
||||
},
|
||||
enabled: !!addr,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
retry: 3,
|
||||
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 8000),
|
||||
});
|
||||
}
|
||||
|
||||
+19
-3
@@ -1,5 +1,5 @@
|
||||
import { useNostr } from '@nostrify/react';
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useCurrentUser } from './useCurrentUser';
|
||||
import { useFeedSettings } from './useFeedSettings';
|
||||
import { useFollowList } from './useFollowActions';
|
||||
@@ -41,6 +41,7 @@ function parseRepostContent(repost: NostrEvent): NostrEvent | undefined {
|
||||
/** Hook to fetch the global or followed feed with infinite scroll pagination. */
|
||||
export function useFeed(tab: 'follows' | 'global') {
|
||||
const { nostr } = useNostr();
|
||||
const queryClient = useQueryClient();
|
||||
const { user } = useCurrentUser();
|
||||
const { data: followData } = useFollowList();
|
||||
const followList = followData?.pubkeys;
|
||||
@@ -130,7 +131,15 @@ export function useFeed(tab: 'follows' | 'global') {
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(seen.values()).sort((a, b) => b.sortTimestamp - a.sortTimestamp);
|
||||
const result = Array.from(seen.values()).sort((a, b) => b.sortTimestamp - a.sortTimestamp);
|
||||
|
||||
// Seed the per-event cache so detail pages resolve instantly,
|
||||
// even for events embedded in reposts that can't be fetched by ID.
|
||||
for (const item of result) {
|
||||
queryClient.setQueryData(['event', item.event.id], item.event);
|
||||
}
|
||||
|
||||
return result;
|
||||
} else {
|
||||
// Global feed — kind 1 notes + user-selected extra kinds
|
||||
const globalKinds = [1, ...extraKinds];
|
||||
@@ -144,10 +153,17 @@ export function useFeed(tab: 'follows' | 'global') {
|
||||
{ signal: querySignal },
|
||||
);
|
||||
|
||||
return events
|
||||
const result = events
|
||||
.filter((ev) => ev.created_at <= now)
|
||||
.sort((a, b) => b.created_at - a.created_at)
|
||||
.map((ev) => ({ event: ev, sortTimestamp: ev.created_at }));
|
||||
|
||||
// Seed the per-event cache so detail pages resolve instantly.
|
||||
for (const item of result) {
|
||||
queryClient.setQueryData(['event', item.event.id], item.event);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
|
||||
@@ -126,13 +126,14 @@ function getParentEventId(event: NostrEvent): string | undefined {
|
||||
}
|
||||
|
||||
export function PostDetailPage({ eventId, relays }: PostDetailPageProps) {
|
||||
const { data: event, isLoading, isError } = useEvent(eventId, relays);
|
||||
const { data: event, isLoading, isError, isFetching } = useEvent(eventId, relays);
|
||||
|
||||
useSeoMeta({
|
||||
title: event ? 'Post Details - Mew' : 'Loading... - Mew',
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
// Show skeleton while loading or retrying
|
||||
if (isLoading || (!event && isFetching)) {
|
||||
return (
|
||||
<MainLayout>
|
||||
<PostDetailShell>
|
||||
@@ -157,7 +158,7 @@ export function PostDetailPage({ eventId, relays }: PostDetailPageProps) {
|
||||
|
||||
/** Detail page for addressable events (naddr). Same layout as PostDetailPage. */
|
||||
export function AddrPostDetailPage({ addr, relays }: AddrPostDetailPageProps) {
|
||||
const { data: event, isLoading, isError } = useAddrEvent(addr, relays);
|
||||
const { data: event, isLoading, isError, isFetching } = useAddrEvent(addr, relays);
|
||||
|
||||
useSeoMeta({
|
||||
title: event
|
||||
@@ -165,7 +166,8 @@ export function AddrPostDetailPage({ addr, relays }: AddrPostDetailPageProps) {
|
||||
: 'Loading... - Mew',
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
// Show skeleton while loading or retrying
|
||||
if (isLoading || (!event && isFetching)) {
|
||||
return (
|
||||
<MainLayout>
|
||||
<PostDetailShell>
|
||||
|
||||
Reference in New Issue
Block a user