import client from '@/services/client.service' import { collectProfilePubkeysFromEvents } from '@/lib/profile-batch-coordinator' import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey' import { NoteFeedProfileContext, type NoteFeedProfileContextValue, useNoteFeedProfileContext } from '@/providers/NoteFeedProfileContext' import { TProfile } from '@/types' import type { Event } from 'nostr-tools' import { useLayoutEffect, useMemo, useRef, useState, type ReactNode } from 'react' const PROFILE_CHUNK = 80 type TBatchState = { profiles: Map pending: Set version: number } function emptyBatch(): TBatchState { return { profiles: new Map(), pending: new Set(), version: 0 } } /** * Batches kind-0 fetches for note/thread views. Seeds run immediately (no debounce) so the * open-note author is in {@link NoteFeedProfileContext.pendingPubkeys} before avatars mount. */ export function ThreadProfileBatchProvider({ seedEvents, children }: { seedEvents: readonly Event[] children: ReactNode }) { const parentNoteFeed = useNoteFeedProfileContext() const loadedRef = useRef>(new Set()) const genRef = useRef(0) const [batch, setBatch] = useState(emptyBatch) const runBatch = (need: string[], gen: number) => { if (need.length === 0) return need.forEach((pk) => loadedRef.current.add(pk)) setBatch((prev) => { const pending = new Set(prev.pending) let changed = false for (const pk of need) { if (!pending.has(pk)) { pending.add(pk) changed = true } } return changed ? { ...prev, pending } : prev }) void (async () => { const chunks: string[][] = [] for (let i = 0; i < need.length; i += PROFILE_CHUNK) { chunks.push(need.slice(i, i + PROFILE_CHUNK)) } const settled = await Promise.allSettled( chunks.map((chunk) => client.fetchProfilesForPubkeys(chunk)) ) if (gen !== genRef.current) return setBatch((prev) => { const next = new Map(prev.profiles) const pend = new Set(prev.pending) settled.forEach((res, idx) => { const chunk = chunks[idx]! if (res.status === 'rejected') { chunk.forEach((pk) => { loadedRef.current.delete(pk) pend.delete(pk) }) return } for (const p of res.value) { const pkNorm = p.pubkey.toLowerCase() next.set(pkNorm, { ...p, pubkey: pkNorm }) pend.delete(pkNorm) } for (const pk of chunk) { const pkNorm = pk.toLowerCase() pend.delete(pkNorm) if (!next.has(pkNorm)) { next.set(pkNorm, { pubkey: pkNorm, npub: pubkeyToNpub(pkNorm) ?? '', username: formatPubkey(pkNorm), batchPlaceholder: true }) } } }) return { profiles: next, pending: pend, version: prev.version + 1 } }) })() } const seedKey = useMemo( () => seedEvents.map((e) => e.id).join('\x1e'), [seedEvents] ) useLayoutEffect(() => { genRef.current += 1 const gen = genRef.current loadedRef.current.clear() setBatch(emptyBatch()) const candidates = collectProfilePubkeysFromEvents(seedEvents) const parentProfiles = parentNoteFeed?.profiles const parentPending = parentNoteFeed?.pendingPubkeys const need = candidates.filter((pk) => { if (parentProfiles?.has(pk)) return false if (parentPending?.has(pk)) return false return true }) runBatch(need, gen) }, [seedKey]) const value = useMemo(() => { const profiles = new Map(parentNoteFeed?.profiles ?? []) for (const [k, v] of batch.profiles) profiles.set(k, v) const pending = new Set(parentNoteFeed?.pendingPubkeys ?? []) batch.pending.forEach((p) => pending.add(p)) return { profiles, pendingPubkeys: pending, version: (parentNoteFeed?.version ?? 0) * 1_000_000 + batch.version } }, [parentNoteFeed, batch]) return {children} }