fix: parallelize author/stats prefetch and stop seeding empty author entries
Two bugs in the useFeed prefetch approach:
1. Empty author seeding was blocking mentions from resolving.
We were calling setQueryData(['author', pk], {}) for any feed post
author with no profile on our relay. If that same pubkey appeared
as a @mention inside another post, useAuthor found the empty {} in
cache, considered it fresh, and never queried — so the mention showed
a generated name forever. Fix: only seed cache when we actually got a
profile event back. Let useAuthor handle the no-profile case itself.
2. Author and stats prefetches were sequential (await author, then await
stats) when they're completely independent. Now run in parallel with
Promise.all — page load time roughly halved for the prefetch step.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
+39
-46
@@ -152,61 +152,54 @@ export function useFeed(tab: 'follows' | 'global') {
|
||||
.map((ev) => ({ event: ev, sortTimestamp: ev.created_at }));
|
||||
}
|
||||
|
||||
// --- Pre-populate author and stats caches ---
|
||||
// By seeding the cache here, inside the queryFn, we guarantee that
|
||||
// ['author', pubkey] and ['event-stats', id] are populated *before*
|
||||
// React re-renders and NoteCard's individual hooks run. This eliminates
|
||||
// the race where NoteCards mount and fire per-card queries before the
|
||||
// batch results arrive.
|
||||
// --- Pre-populate author and stats caches in parallel ---
|
||||
// Seeding here, inside the queryFn, guarantees caches are populated
|
||||
// before React re-renders and NoteCard's individual hooks run —
|
||||
// eliminating the race where per-card queries fire before batch results arrive.
|
||||
// We only fetch what isn't already cached, and we never seed empty entries
|
||||
// so that mentions / quoted-note authors can still resolve via useAuthor.
|
||||
|
||||
// Authors — only fetch pubkeys not already cached
|
||||
const pubkeysToFetch = [...new Set(
|
||||
items.flatMap((item) => item.repostedBy ? [item.event.pubkey, item.repostedBy] : [item.event.pubkey]),
|
||||
)].filter((pk) => queryClient.getQueryData(['author', pk]) === undefined);
|
||||
|
||||
if (pubkeysToFetch.length > 0) {
|
||||
try {
|
||||
const profileEvents = await nostr.query(
|
||||
[{ kinds: [0], authors: pubkeysToFetch, limit: pubkeysToFetch.length }],
|
||||
{ signal: AbortSignal.any([signal, AbortSignal.timeout(5000)]) },
|
||||
);
|
||||
for (const ev of profileEvents) {
|
||||
let metadata: NostrMetadata | undefined;
|
||||
try { metadata = n.json().pipe(n.metadata()).parse(ev.content); } catch { /* skip */ }
|
||||
queryClient.setQueryData(['author', ev.pubkey], { event: ev, metadata });
|
||||
}
|
||||
// Seed empty entry for any pubkey that returned no profile
|
||||
for (const pk of pubkeysToFetch) {
|
||||
if (queryClient.getQueryData(['author', pk]) === undefined) {
|
||||
queryClient.setQueryData(['author', pk], {});
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Timeout or abort — NoteCard's individual useAuthor will handle it
|
||||
}
|
||||
}
|
||||
|
||||
// Stats — only fetch event IDs not already cached
|
||||
const eventIdsToFetch = items
|
||||
.map((item) => item.event.id)
|
||||
.filter((id) => queryClient.getQueryData(['event-stats', id]) === undefined);
|
||||
|
||||
if (eventIdsToFetch.length > 0) {
|
||||
try {
|
||||
const statEvents = await nostr.query(
|
||||
[
|
||||
{ kinds: [1, 6, 7, 9735], '#e': eventIdsToFetch, limit: eventIdsToFetch.length * 10 },
|
||||
{ kinds: [1], '#q': eventIdsToFetch, limit: eventIdsToFetch.length * 3 },
|
||||
],
|
||||
{ signal: AbortSignal.any([signal, AbortSignal.timeout(6000)]) },
|
||||
);
|
||||
for (const id of eventIdsToFetch) {
|
||||
queryClient.setQueryData(['event-stats', id], computePageStats(id, statEvents));
|
||||
}
|
||||
} catch {
|
||||
// Timeout or abort — NoteCard's individual useEventStats will handle it
|
||||
}
|
||||
}
|
||||
await Promise.all([
|
||||
// Fetch author profiles
|
||||
pubkeysToFetch.length > 0
|
||||
? nostr.query(
|
||||
[{ kinds: [0], authors: pubkeysToFetch, limit: pubkeysToFetch.length }],
|
||||
{ signal: AbortSignal.any([signal, AbortSignal.timeout(5000)]) },
|
||||
).then((profileEvents) => {
|
||||
for (const ev of profileEvents) {
|
||||
let metadata: NostrMetadata | undefined;
|
||||
try { metadata = n.json().pipe(n.metadata()).parse(ev.content); } catch { /* skip */ }
|
||||
queryClient.setQueryData(['author', ev.pubkey], { event: ev, metadata });
|
||||
}
|
||||
// Note: we intentionally do NOT seed {} for pubkeys with no profile.
|
||||
// Doing so would block useAuthor from querying for mentioned users
|
||||
// whose pubkeys weren't in the feed events themselves.
|
||||
}).catch(() => { /* timeout — per-card useAuthor will handle it */ })
|
||||
: Promise.resolve(),
|
||||
|
||||
// Fetch interaction stats
|
||||
eventIdsToFetch.length > 0
|
||||
? nostr.query(
|
||||
[
|
||||
{ kinds: [1, 6, 7, 9735], '#e': eventIdsToFetch, limit: eventIdsToFetch.length * 10 },
|
||||
{ kinds: [1], '#q': eventIdsToFetch, limit: eventIdsToFetch.length * 3 },
|
||||
],
|
||||
{ signal: AbortSignal.any([signal, AbortSignal.timeout(6000)]) },
|
||||
).then((statEvents) => {
|
||||
for (const id of eventIdsToFetch) {
|
||||
queryClient.setQueryData(['event-stats', id], computePageStats(id, statEvents));
|
||||
}
|
||||
}).catch(() => { /* timeout — per-card useEventStats will handle it */ })
|
||||
: Promise.resolve(),
|
||||
]);
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user