Skip already-cached pubkeys in useAuthors batch query

Only send uncached pubkeys to the relay. As feed pages load, profiles
fetched by earlier pages are already in the ['author', pubkey] cache,
so subsequent pages only request the new authors they introduce.

Also seeds empty entries for pubkeys with no kind 0 result to avoid
re-fetching them on the next page load.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy
2026-02-18 00:41:10 -06:00
parent ee69816dd9
commit 94b43ac615
+53 -27
View File
@@ -1,5 +1,6 @@
import { type NostrEvent, type NostrMetadata, NSchema as n } from '@nostrify/nostrify';
import { useNostr } from '@nostrify/react';
import { useEffect, useMemo, useRef } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
export interface AuthorData {
@@ -11,64 +12,89 @@ export interface AuthorData {
/**
* Batch-fetch multiple author profiles in a single relay query.
*
* Much more efficient than calling `useAuthor` once per pubkey — one
* round-trip instead of N. Results are also seeded into the individual
* `['author', pubkey]` cache entries so that later `useAuthor()` calls
* for the same pubkeys resolve instantly from cache.
* Only fetches pubkeys that aren't already in the `['author', pubkey]`
* cache — so scrolling through pages of a feed never re-fetches profiles
* that were already loaded by earlier pages.
*
* @param pubkeys - Array of hex pubkeys to fetch profiles for
* @returns React Query result with a Map<pubkey, AuthorData>
* Results are seeded into the individual `['author', pubkey]` cache so
* that `useAuthor()` calls resolve instantly from cache.
*/
export function useAuthors(pubkeys: string[]) {
const { nostr } = useNostr();
const queryClient = useQueryClient();
// Deduplicate and sort for a stable query key
const uniquePubkeys = [...new Set(pubkeys)].sort();
// Deduplicate
const uniquePubkeys = useMemo(() => [...new Set(pubkeys)], [pubkeys]);
return useQuery<Map<string, AuthorData>>({
queryKey: ['authors', uniquePubkeys.join(',')],
// Filter out pubkeys that already have a cached ['author', pubkey] entry
const uncachedPubkeys = useMemo(() => {
return uniquePubkeys.filter(
(pk) => !queryClient.getQueryData(['author', pk]),
);
}, [uniquePubkeys, queryClient]);
// Stable key: only the uncached pubkeys, sorted
const sortedUncached = useMemo(() => [...uncachedPubkeys].sort(), [uncachedPubkeys]);
const query = useQuery<Map<string, AuthorData>>({
queryKey: ['authors-batch', sortedUncached.join(',')],
queryFn: async ({ signal }) => {
if (uniquePubkeys.length === 0) {
return new Map();
}
if (sortedUncached.length === 0) return new Map();
const combinedSignal = AbortSignal.any([signal, AbortSignal.timeout(5000)]);
const events = await nostr.query(
[{ kinds: [0], authors: uniquePubkeys, limit: uniquePubkeys.length }],
[{ kinds: [0], authors: sortedUncached, limit: sortedUncached.length }],
{ signal: combinedSignal },
);
const authorMap = new Map<string, AuthorData>();
// Initialize every requested pubkey so callers always get an entry
for (const pk of uniquePubkeys) {
authorMap.set(pk, { pubkey: pk });
}
// Process returned kind-0 events
for (const event of events) {
let metadata: NostrMetadata | undefined;
try {
metadata = n.json().pipe(n.metadata()).parse(event.content);
} catch {
// unparseable — leave metadata undefined
// unparseable
}
const data: AuthorData = { pubkey: event.pubkey, event, metadata };
authorMap.set(event.pubkey, data);
// Seed the individual `useAuthor(pubkey)` cache so it resolves
// instantly for any card that mounts later.
queryClient.setQueryData(['author', event.pubkey], { event, metadata });
}
return authorMap;
},
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
enabled: uniquePubkeys.length > 0,
placeholderData: (prev) => prev,
enabled: sortedUncached.length > 0,
});
// Seed individual ['author', pubkey] caches when batch resolves.
// Use a ref to track what we've already seeded to avoid re-seeding.
const seededRef = useRef(new Set<string>());
useEffect(() => {
if (!query.data) return;
for (const [pubkey, data] of query.data) {
if (!seededRef.current.has(pubkey)) {
seededRef.current.add(pubkey);
queryClient.setQueryData(['author', pubkey], {
event: data.event,
metadata: data.metadata,
});
}
}
// Also seed empty entries for pubkeys we asked for but got no result
// so we don't re-fetch them next time.
for (const pk of sortedUncached) {
if (!seededRef.current.has(pk) && !query.data.has(pk)) {
seededRef.current.add(pk);
queryClient.setQueryData(['author', pk], { event: undefined, metadata: undefined });
}
}
}, [query.data, sortedUncached, queryClient]);
return query;
}