You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

137 lines
4.2 KiB

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<string, TProfile>
pending: Set<string>
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<Set<string>>(new Set())
const genRef = useRef(0)
const [batch, setBatch] = useState<TBatchState>(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<NoteFeedProfileContextValue>(() => {
const profiles = new Map<string, TProfile>(parentNoteFeed?.profiles ?? [])
for (const [k, v] of batch.profiles) profiles.set(k, v)
const pending = new Set<string>(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 <NoteFeedProfileContext.Provider value={value}>{children}</NoteFeedProfileContext.Provider>
}