Browse Source

performance fixes

imwald
Silberengel 7 days ago
parent
commit
f363551af9
  1. 157
      src/components/Library/LibrarySearchBar.tsx
  2. 58
      src/hooks/useLibraryPublications.ts
  3. 1
      src/i18n/locales/de.ts
  4. 1
      src/i18n/locales/en.ts
  5. 192
      src/lib/library-publication-index.ts
  6. 11
      src/pages/primary/LibraryPage/index.tsx

157
src/components/Library/LibrarySearchBar.tsx

@ -5,15 +5,10 @@ import { Switch } from '@/components/ui/switch' @@ -5,15 +5,10 @@ import { Switch } from '@/components/ui/switch'
import { normalizeToDTag } from '@/lib/search-parser'
import type { LibraryPublicationRelaySearchAxis } from '@/lib/library-publication-index'
import { cn } from '@/lib/utils'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import modalManager from '@/services/modal-manager.service'
import { randomString } from '@/lib/random'
import { FileText, Loader2, Search, User, Wifi } from 'lucide-react'
import {
HTMLAttributes,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState
@ -29,8 +24,9 @@ type LibrarySearchOption = { @@ -29,8 +24,9 @@ type LibrarySearchOption = {
export default function LibrarySearchBar({
searchQuery,
onSearchQueryChange,
committedSearch,
searchAxis,
onSearchAxisChange,
onCommitSearch,
showOnlyMine,
onShowOnlyMineChange,
mineFilterLoading,
@ -40,8 +36,9 @@ export default function LibrarySearchBar({ @@ -40,8 +36,9 @@ export default function LibrarySearchBar({
}: {
searchQuery: string
onSearchQueryChange: (value: string) => void
committedSearch: string
searchAxis: LibraryPublicationRelaySearchAxis | null
onSearchAxisChange: (axis: LibraryPublicationRelaySearchAxis | null) => void
onCommitSearch: (query: string, axis: LibraryPublicationRelaySearchAxis | null) => void
showOnlyMine: boolean
onShowOnlyMineChange: (value: boolean) => void
mineFilterLoading?: boolean
@ -50,29 +47,17 @@ export default function LibrarySearchBar({ @@ -50,29 +47,17 @@ export default function LibrarySearchBar({
disabled?: boolean
}) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const [searching, setSearching] = useState(false)
const [displayList, setDisplayList] = useState(false)
const [selectableOptions, setSelectableOptions] = useState<LibrarySearchOption[]>([])
const [selectedIndex, setSelectedIndex] = useState(-1)
const prevSelectableCountRef = useRef(0)
const searchInputRef = useRef<HTMLInputElement>(null)
const barContainerRef = useRef<HTMLDivElement>(null)
const [suggestPanelTop, setSuggestPanelTop] = useState(0)
const id = useMemo(() => `library-search-${randomString()}`, [])
const canSearchRelays = searchQuery.trim().length > 0 && !relaySearchLoading
useEffect(() => {
const selectableOptions = useMemo((): LibrarySearchOption[] => {
const search = searchQuery.trim()
if (!search) {
setSelectableOptions([])
setSelectedIndex(-1)
setSearching(false)
return
}
if (!search) return []
const normalizedDTag = normalizeToDTag(search)
const options: LibrarySearchOption[] = [
return [
{ axis: null, search },
{ axis: 'title', search },
{ axis: 'author', search },
@ -80,71 +65,23 @@ export default function LibrarySearchBar({ @@ -80,71 +65,23 @@ export default function LibrarySearchBar({
? [{ axis: 'd-tag' as const, search: normalizedDTag, input: search }]
: [])
]
setSelectableOptions(options)
}, [searchQuery])
useEffect(() => {
setDisplayList(searching && !!searchQuery.trim())
}, [searching, searchQuery])
useEffect(() => {
const trimmed = searchQuery.trim()
const len = selectableOptions.length
if (!trimmed) {
prevSelectableCountRef.current = 0
return
}
if (len > 0 && prevSelectableCountRef.current === 0) {
const el = searchInputRef.current
if (el && document.activeElement !== el) {
queueMicrotask(() => {
el.focus({ preventScroll: true })
})
}
}
prevSelectableCountRef.current = len
}, [searchQuery, selectableOptions])
useEffect(() => {
if (displayList && selectableOptions.length > 0) {
modalManager.register(id, () => {
setDisplayList(false)
})
} else {
modalManager.unregister(id)
}
}, [displayList, selectableOptions.length, id])
const displayList = searching && selectableOptions.length > 0
const blur = () => {
setSearching(false)
setSelectedIndex(-1)
searchInputRef.current?.blur()
}
const applyOption = (option: LibrarySearchOption) => {
onSearchAxisChange(option.axis)
if (option.input && option.input !== searchQuery) {
onSearchQueryChange(option.input)
}
const applyOption = useCallback(
(option: LibrarySearchOption) => {
onCommitSearch(option.input ?? option.search, option.axis)
blur()
}
const updateSuggestPanelGeometry = useCallback(() => {
const el = barContainerRef.current
if (!el) return
setSuggestPanelTop(el.getBoundingClientRect().bottom)
}, [])
useLayoutEffect(() => {
if (!displayList || selectableOptions.length === 0 || !isSmallScreen) return
updateSuggestPanelGeometry()
const onScrollOrResize = () => updateSuggestPanelGeometry()
window.addEventListener('scroll', onScrollOrResize, true)
window.addEventListener('resize', onScrollOrResize)
return () => {
window.removeEventListener('scroll', onScrollOrResize, true)
window.removeEventListener('resize', onScrollOrResize)
}
}, [displayList, selectableOptions.length, isSmallScreen, searchQuery, updateSuggestPanelGeometry])
},
[onCommitSearch]
)
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
@ -173,7 +110,7 @@ export default function LibrarySearchBar({ @@ -173,7 +110,7 @@ export default function LibrarySearchBar({
blur()
}
},
[selectableOptions, selectedIndex]
[applyOption, selectableOptions, selectedIndex]
)
const list = useMemo(() => {
@ -222,61 +159,37 @@ export default function LibrarySearchBar({ @@ -222,61 +159,37 @@ export default function LibrarySearchBar({
})}
</>
)
}, [selectableOptions, selectedIndex])
}, [applyOption, selectableOptions, selectedIndex])
const suggestTopPx = Math.max(0, suggestPanelTop - 4)
const suggestionsPanel = list ? (
<div
className={cn(
'bg-surface-background shadow-lg',
isSmallScreen
? 'fixed left-4 right-4 z-[110] overflow-y-auto rounded-b-lg border border-t-0 border-border/80 pt-1'
: 'absolute top-full z-50 -translate-y-1 inset-x-0 rounded-b-lg pt-1'
)}
style={
isSmallScreen
? {
top: suggestTopPx,
maxHeight: `calc(100dvh - ${suggestTopPx}px - 3.25rem - env(safe-area-inset-bottom, 0px))`
}
: undefined
}
onMouseDown={(e) => e.preventDefault()}
>
<div className="h-fit">{list}</div>
</div>
) : null
const isCommitted = committedSearch.trim().length > 0 && committedSearch.trim() === searchQuery.trim()
const scopeLabel =
searchAxis === 'title'
isCommitted && searchAxis === 'title'
? t('Library search scope title')
: searchAxis === 'author'
: isCommitted && searchAxis === 'author'
? t('Library search scope author')
: searchAxis === 'd-tag'
: isCommitted && searchAxis === 'd-tag'
? t('Library search scope dtag')
: null
return (
<div className="space-y-3">
<div ref={barContainerRef} className="relative">
{displayList && list && !isSmallScreen && (
<>
{suggestionsPanel}
<div className="fixed inset-0 z-40 w-full h-full" onClick={() => blur()} aria-hidden />
</>
)}
{displayList && list && isSmallScreen && (
<>
<div className="fixed inset-0 z-[100] w-full h-full" onClick={() => blur()} aria-hidden />
{suggestionsPanel}
</>
)}
<div className="relative">
{displayList && list ? (
<div
className="absolute top-full z-50 -translate-y-1 inset-x-0 rounded-b-lg border border-border/80 bg-surface-background pt-1 shadow-lg"
onMouseDown={(e) => e.preventDefault()}
>
<div className="h-fit">{list}</div>
</div>
) : null}
<SearchInput
ref={searchInputRef}
type="search"
value={searchQuery}
onChange={(e) => {
setSearching(true)
setSelectedIndex(-1)
onSearchQueryChange(e.target.value)
}}
onPaste={() => setSearching(true)}
@ -284,17 +197,15 @@ export default function LibrarySearchBar({ @@ -284,17 +197,15 @@ export default function LibrarySearchBar({
onFocus={() => setSearching(true)}
onBlur={() => setSearching(false)}
placeholder={t('Library search placeholder')}
className={cn(
'bg-surface-background pl-3',
displayList && isSmallScreen && 'relative z-[120]',
displayList && !isSmallScreen && 'z-50'
)}
className={cn('bg-surface-background pl-3', displayList && 'z-50')}
disabled={disabled}
aria-label={t('Library search placeholder')}
/>
</div>
{scopeLabel ? (
<p className="text-xs text-muted-foreground">{scopeLabel}</p>
) : searchQuery.trim() && !isCommitted ? (
<p className="text-xs text-muted-foreground">{t('Library search commit hint')}</p>
) : null}
{onSearchRelays ? (
<Button

58
src/hooks/useLibraryPublications.ts

@ -25,6 +25,7 @@ import type { Event } from 'nostr-tools' @@ -25,6 +25,7 @@ import type { Event } from 'nostr-tools'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
const SEARCH_DEBOUNCE_MS = 300
const RELAY_SEARCH_TIMEOUT_MS = 30_000
const EMPTY_ENGAGEMENT: PublicationEngagementMaps = {
labelAddresses: new Set(),
@ -60,11 +61,11 @@ export function useLibraryPublications(isActive: boolean) { @@ -60,11 +61,11 @@ export function useLibraryPublications(isActive: boolean) {
const [indexEvents, setIndexEvents] = useState<Event[]>([])
const [engagement, setEngagement] = useState<PublicationEngagementMaps>(EMPTY_ENGAGEMENT)
const [searchQuery, setSearchQuery] = useState('')
const [committedSearch, setCommittedSearch] = useState('')
const [searchAxis, setSearchAxis] = useState<LibraryPublicationRelaySearchAxis | null>(null)
const [debouncedSearch, setDebouncedSearch] = useState('')
const [showOnlyMine, setShowOnlyMine] = useState(false)
const [loading, setLoading] = useState(false)
const [engagementLoading, setEngagementLoading] = useState(false)
const [searchLoading, setSearchLoading] = useState(false)
const [relaySearchLoading, setRelaySearchLoading] = useState(false)
const [searchResults, setSearchResults] = useState<LibraryPublicationEntry[] | null>(null)
@ -132,16 +133,28 @@ export function useLibraryPublications(isActive: boolean) { @@ -132,16 +133,28 @@ export function useLibraryPublications(isActive: boolean) {
}, [pubkey])
useEffect(() => {
const t = window.setTimeout(() => setDebouncedSearch(searchQuery), SEARCH_DEBOUNCE_MS)
const t = window.setTimeout(() => setDebouncedSearch(committedSearch), SEARCH_DEBOUNCE_MS)
return () => window.clearTimeout(t)
}, [searchQuery])
}, [committedSearch])
useEffect(() => {
if (!searchQuery.trim()) {
setCommittedSearch('')
setSearchAxis(null)
}
}, [searchQuery])
const commitSearch = useCallback(
(query: string, axis: LibraryPublicationRelaySearchAxis | null) => {
const trimmed = query.trim()
if (!trimmed) return
setSearchQuery(trimmed)
setCommittedSearch(trimmed)
setSearchAxis(axis)
},
[]
)
useEffect(() => {
setFeedPageIndex(0)
}, [debouncedSearch, showOnlyMine, searchAxis])
@ -179,7 +192,6 @@ export function useLibraryPublications(isActive: boolean) { @@ -179,7 +192,6 @@ export function useLibraryPublications(isActive: boolean) {
let cancelled = false
indexesReadyRef.current = false
setLoading(true)
setEngagementLoading(false)
setError(null)
setFeedPageIndex(0)
const forceRefresh = forceRefreshNextLoadRef.current
@ -207,12 +219,19 @@ export function useLibraryPublications(isActive: boolean) { @@ -207,12 +219,19 @@ export function useLibraryPublications(isActive: boolean) {
}
applyIndexesSnapshot(snapshot, EMPTY_ENGAGEMENT, 0)
setLoading(false)
setEngagementLoading(true)
}
})
if (cancelled) return
applyIndexesSnapshot(result, result.engagement, 0)
setEngagement(result.engagement)
applyIndexesSnapshot(
{
indexEvents: result.indexEvents,
allIndexCount: result.allIndexCount,
topLevelCount: result.topLevelCount
},
result.engagement,
0
)
} catch (e) {
if (cancelled) return
if (indexesReadyRef.current) {
@ -231,7 +250,6 @@ export function useLibraryPublications(isActive: boolean) { @@ -231,7 +250,6 @@ export function useLibraryPublications(isActive: boolean) {
} finally {
if (!cancelled) {
setLoading(false)
setEngagementLoading(false)
}
}
})()
@ -304,16 +322,34 @@ export function useLibraryPublications(isActive: boolean) { @@ -304,16 +322,34 @@ export function useLibraryPublications(isActive: boolean) {
const searchOnRelays = useCallback(async () => {
const q = searchQuery.trim()
if (!q) return
setCommittedSearch(q)
setRelaySearchLoading(true)
setError(null)
try {
const relays = await buildLibraryRelayUrls(pubkey || undefined, blockedRelays ?? [])
const { events, mergedIndexEvents, fromCache } = await searchLibraryPublicationsOnRelays(
let timeoutId: number | undefined
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutId = window.setTimeout(
() => reject(new Error('Relay search timed out')),
RELAY_SEARCH_TIMEOUT_MS
)
})
let events: Event[]
let mergedIndexEvents: Event[]
let fromCache: boolean
try {
;({ events, mergedIndexEvents, fromCache } = await Promise.race([
searchLibraryPublicationsOnRelays(
q,
relays,
{ indexEvents, engagement },
{ axis: searchAxis }
)
),
timeoutPromise
]))
} finally {
if (timeoutId !== undefined) window.clearTimeout(timeoutId)
}
setIndexEvents(mergedIndexEvents)
setAllIndexCount(mergedIndexEvents.length)
setTopLevelCount(getTopLevelIndexEvents(mergedIndexEvents).length)
@ -444,14 +480,14 @@ export function useLibraryPublications(isActive: boolean) { @@ -444,14 +480,14 @@ export function useLibraryPublications(isActive: boolean) {
entries: filteredEntries,
searchQuery,
setSearchQuery,
committedSearch,
searchAxis,
setSearchAxis,
commitSearch,
showOnlyMine,
setShowOnlyMine,
mineFilterLoading:
mineFilterComputing || (showOnlyMine && booklistTargetsLoading),
loading,
engagementLoading,
searchLoading,
relaySearchLoading,
error,

1
src/i18n/locales/de.ts

@ -1657,6 +1657,7 @@ export default { @@ -1657,6 +1657,7 @@ export default {
'Library search scope title': 'Suche nach Titel',
'Library search scope author': 'Suche nach Autor',
'Library search scope dtag': 'Suche nach d-Tag',
'Library search commit hint': 'Enter drücken oder einen Suchtyp auswählen',
'Library show only my publications': 'Meine Publikationen',
'Library empty': 'Noch keine Publikationen auf deinen Relays gefunden.',
'Library empty filtered': 'Keine Publikationen entsprechen den Filtern.',

1
src/i18n/locales/en.ts

@ -1680,6 +1680,7 @@ export default { @@ -1680,6 +1680,7 @@ export default {
'Library search scope title': 'Searching by title',
'Library search scope author': 'Searching by author',
'Library search scope dtag': 'Searching by d-tag',
'Library search commit hint': 'Press Enter or choose a search type below',
'Library show only my publications': 'My publications',
'Library empty': 'No publications found on your relays yet.',
'Library empty filtered': 'No publications match your filters.',

192
src/lib/library-publication-index.ts

@ -12,7 +12,6 @@ import { queryIndexRelay, queryIndexRelayForLibrary, queryIndexRelayPublicationM @@ -12,7 +12,6 @@ import { queryIndexRelay, queryIndexRelayForLibrary, queryIndexRelayPublicationM
import {
buildIndexByAddress,
buildStructuralPublicationIndexMap,
collectPublicationIndexEventIds,
collectReachableAddressesCached,
eventTagAddress,
filterValidIndexEvents,
@ -55,7 +54,11 @@ const INDEX_MAX_PAGES_PER_RELAY = 100 @@ -55,7 +54,11 @@ const INDEX_MAX_PAGES_PER_RELAY = 100
const INDEX_VERIFY_CHUNK = 80
const ENGAGEMENT_ADDRESS_CHUNK = 36
const ENGAGEMENT_EVENT_ID_CHUNK = 44
const MAX_TARGET_ADDRESSES = 480
/** Cap engagement relay queries to the first slice of the catalog (not the full index corpus). */
const MAX_TARGET_ADDRESSES = 120
const MAX_TARGET_EVENT_IDS = 160
const MAX_ENGAGEMENT_HTTP_CHUNKS = 6
const ENGAGEMENT_FETCH_TIMEOUT_MS = 25_000
const HYDRATE_MISSING_CAP = 64
export const LIBRARY_PAGE_SIZE = 120
/** @deprecated Use {@link LIBRARY_PAGE_SIZE} */
@ -918,8 +921,21 @@ export async function fetchPublicationEngagementMaps( @@ -918,8 +921,21 @@ export async function fetchPublicationEngagementMaps(
return emptyPublicationEngagementMaps()
}
const addressChunks = chunkArray([...targetAddresses], ENGAGEMENT_ADDRESS_CHUNK)
const eventIdChunks = chunkArray([...targetEventIds], ENGAGEMENT_EVENT_ID_CHUNK)
return withEngagementTimeout(
fetchPublicationEngagementMapsInner(relayUrls, targetAddresses, targetEventIds, options),
emptyPublicationEngagementMaps(),
'maps'
)
}
async function fetchPublicationEngagementMapsInner(
relayUrls: string[],
targetAddresses: Set<string>,
targetEventIds: Set<string>,
options?: { viewerPubkey?: string | null }
): Promise<PublicationEngagementMaps> {
const addressChunks = limitEngagementChunks(chunkArray([...targetAddresses], ENGAGEMENT_ADDRESS_CHUNK))
const eventIdChunks = limitEngagementChunks(chunkArray([...targetEventIds], ENGAGEMENT_EVENT_ID_CHUNK))
const { wsRelays, httpRelays } = splitWsAndHttpRelays(relayUrls)
const useWsEngagement = wsRelays.length > 0
if (import.meta.env.DEV) {
@ -1572,8 +1588,10 @@ export async function refreshLibraryEngagement( @@ -1572,8 +1588,10 @@ export async function refreshLibraryEngagement(
viewerPubkey?: string | null
): Promise<{ engagement: PublicationEngagementMaps; engaged: LibraryPublicationEntry[] }> {
const indexByAddress = buildIndexByAddress(indexEvents)
const targetAddresses = collectTargetAddressesFromIndexes(indexEvents, indexByAddress)
const targetEventIds = collectPublicationIndexEventIds(indexEvents)
const { addresses: targetAddresses, eventIds: targetEventIds } = collectEngagementTargets(
indexEvents,
indexByAddress
)
const engagementRelayUrls = await buildLibraryEngagementRelayUrls(
viewerPubkey ?? undefined,
indexRelayUrls,
@ -1598,22 +1616,22 @@ export async function refreshLibraryEngagement( @@ -1598,22 +1616,22 @@ export async function refreshLibraryEngagement(
}
}
/** Search all cached kind-30040 indexes (library index store), mapping nested hits to top-level roots. */
export function searchLibraryPublicationIndex(
const LIBRARY_SEARCH_BATCH_SIZE = 80
function collectLibraryPublicationIndexSearchRoots(
query: string,
indexEvents: Event[],
indexByAddress: Map<string, Event>,
axis?: LibraryPublicationRelaySearchAxis | null
): Event[] {
topLevelIds: Set<string>,
addressToRoot: Map<string, Event>,
axis: LibraryPublicationRelaySearchAxis | null | undefined,
roots: Map<string, Event>,
start: number,
end: number
): void {
const q = query.trim()
if (!q || indexEvents.length === 0) return []
const topLevel = getTopLevelIndexEvents(indexEvents)
const topLevelIds = new Set(topLevel.map((ev) => ev.id))
const addressToRoot = buildAddressToRootMap(topLevel, indexByAddress)
const roots = new Map<string, Event>()
for (const ev of indexEvents) {
for (let i = start; i < end; i++) {
const ev = indexEvents[i]
if (ev.kind !== ExtendedKind.PUBLICATION) continue
if (!publicationIndexMatchesSearchQueryWithAxis(ev, q, axis)) continue
@ -1626,10 +1644,78 @@ export function searchLibraryPublicationIndex( @@ -1626,10 +1644,78 @@ export function searchLibraryPublicationIndex(
const root = addr ? addressToRoot.get(addr) : undefined
if (root) roots.set(root.id, root)
}
}
/** Search all cached kind-30040 indexes (library index store), mapping nested hits to top-level roots. */
export function searchLibraryPublicationIndex(
query: string,
indexEvents: Event[],
indexByAddress: Map<string, Event>,
axis?: LibraryPublicationRelaySearchAxis | null
): Event[] {
const q = query.trim()
if (!q || indexEvents.length === 0) return []
const topLevel = getTopLevelIndexEvents(indexEvents)
const topLevelIds = new Set(topLevel.map((ev) => ev.id))
const addressToRoot = buildAddressToRootMap(topLevel, indexByAddress)
const roots = new Map<string, Event>()
collectLibraryPublicationIndexSearchRoots(
q,
indexEvents,
topLevelIds,
addressToRoot,
axis,
roots,
0,
indexEvents.length
)
return [...roots.values()]
}
/** Yields between batches so large index scans do not freeze the UI. */
export function searchLibraryPublicationIndexAsync(
query: string,
indexEvents: Event[],
indexByAddress: Map<string, Event>,
axis?: LibraryPublicationRelaySearchAxis | null,
signal?: { cancelled: boolean }
): Promise<Event[]> {
const q = query.trim()
if (!q || indexEvents.length === 0) return Promise.resolve([])
const topLevel = getTopLevelIndexEvents(indexEvents)
const topLevelIds = new Set(topLevel.map((ev) => ev.id))
const addressToRoot = buildAddressToRootMap(topLevel, indexByAddress)
const roots = new Map<string, Event>()
let i = 0
return new Promise((resolve) => {
const step = () => {
if (signal?.cancelled) return
const end = Math.min(i + LIBRARY_SEARCH_BATCH_SIZE, indexEvents.length)
collectLibraryPublicationIndexSearchRoots(
q,
indexEvents,
topLevelIds,
addressToRoot,
axis,
roots,
i,
end
)
i = end
if (signal?.cancelled) return
if (i < indexEvents.length) {
requestAnimationFrame(step)
} else {
resolve([...roots.values()])
}
}
requestAnimationFrame(step)
})
}
export type LibrarySearchContext = {
indexEvents: Event[]
engagement?: PublicationEngagementMaps
@ -1666,7 +1752,7 @@ export async function searchLibraryPublications( @@ -1666,7 +1752,7 @@ export async function searchLibraryPublications(
const engagement = context.engagement ?? EMPTY_ENGAGEMENT
const indexByAddress = buildIndexByAddress(indexEvents)
const fromIndex = searchLibraryPublicationIndex(q, indexEvents, indexByAddress, axis)
const fromIndex = await searchLibraryPublicationIndexAsync(q, indexEvents, indexByAddress, axis)
const rootMap = new Map<string, Event>()
for (const root of fromIndex) rootMap.set(root.id, root)
@ -2232,19 +2318,66 @@ function collectTargetAddressesFromIndexes( @@ -2232,19 +2318,66 @@ function collectTargetAddressesFromIndexes(
indexEvents: Event[],
indexByAddress: Map<string, Event>
): Set<string> {
return collectEngagementTargets(indexEvents, indexByAddress).addresses
}
/** Capped address + event-id targets for label/comment/highlight relay queries. */
export function collectEngagementTargets(
indexEvents: Event[],
indexByAddress: Map<string, Event>
): { addresses: Set<string>; eventIds: Set<string> } {
const addresses = new Set<string>()
const eventIds = new Set<string>()
outer: for (const root of getTopLevelIndexEvents(indexEvents)) {
eventIds.add(root.id.toLowerCase())
if (eventIds.size >= MAX_TARGET_EVENT_IDS) break
for (const addr of collectReachableAddressesCached(root, indexByAddress)) {
addresses.add(addr)
if (addresses.size >= MAX_TARGET_ADDRESSES) break outer
const indexed = indexByAddress.get(addr)
if (indexed) eventIds.add(indexed.id.toLowerCase())
if (addresses.size >= MAX_TARGET_ADDRESSES || eventIds.size >= MAX_TARGET_EVENT_IDS) {
break outer
}
}
const rootAddr = eventTagAddress(root)
if (rootAddr) {
addresses.add(rootAddr)
if (addresses.size >= MAX_TARGET_ADDRESSES) break outer
if (addresses.size >= MAX_TARGET_ADDRESSES || eventIds.size >= MAX_TARGET_EVENT_IDS) {
break outer
}
}
}
return addresses
return { addresses, eventIds }
}
function limitEngagementChunks<T>(chunks: T[][]): T[][] {
return chunks.length <= MAX_ENGAGEMENT_HTTP_CHUNKS
? chunks
: chunks.slice(0, MAX_ENGAGEMENT_HTTP_CHUNKS)
}
async function withEngagementTimeout<T>(
promise: Promise<T>,
fallback: T,
label: string
): Promise<T> {
let timer: ReturnType<typeof setTimeout> | undefined
try {
return await Promise.race([
promise,
new Promise<T>((resolve) => {
timer = setTimeout(() => {
if (import.meta.env.DEV) {
logger.warn('[Library] engagement fetch timed out', { label, ms: ENGAGEMENT_FETCH_TIMEOUT_MS })
}
resolve(fallback)
}, ENGAGEMENT_FETCH_TIMEOUT_MS)
})
])
} finally {
if (timer) clearTimeout(timer)
}
}
async function buildEngagedFromCache(
@ -2257,8 +2390,10 @@ async function buildEngagedFromCache( @@ -2257,8 +2390,10 @@ async function buildEngagedFromCache(
const topLevel = getTopLevelIndexEvents(indexEvents)
let maps = engagement
if (!maps) {
const targetAddresses = collectTargetAddressesFromIndexes(indexEvents, indexByAddress)
const targetEventIds = collectPublicationIndexEventIds(indexEvents)
const { addresses: targetAddresses, eventIds: targetEventIds } = collectEngagementTargets(
indexEvents,
indexByAddress
)
const engagementRelayUrls = await buildLibraryEngagementRelayUrls(
viewerPubkey ?? undefined,
indexRelayUrls,
@ -2335,11 +2470,10 @@ async function runLibraryPublicationIndexLoad( @@ -2335,11 +2470,10 @@ async function runLibraryPublicationIndexLoad(
if (!options?.forceRefresh && sessionCache?.relayKey === key) {
const cachedIndexEvents = indexEventsFromCache(sessionCache)
if (sessionCache.viewerPubkey !== viewerPubkey) {
const targetAddresses = collectTargetAddressesFromIndexes(
const { addresses: targetAddresses, eventIds: targetEventIds } = collectEngagementTargets(
cachedIndexEvents,
sessionCache.indexByAddress
)
const targetEventIds = collectPublicationIndexEventIds(cachedIndexEvents)
const engagementRelayUrls = await buildLibraryEngagementRelayUrls(
viewerPubkey ?? undefined,
relayUrls,
@ -2403,8 +2537,10 @@ async function runLibraryPublicationIndexLoad( @@ -2403,8 +2537,10 @@ async function runLibraryPublicationIndexLoad(
}
topLevel = getTopLevelIndexEventsFromMap(indexByAddress)
const targetAddresses = collectTargetAddressesFromIndexes(indexEvents, indexByAddress)
const targetEventIds = collectPublicationIndexEventIds(indexEvents)
const { addresses: targetAddresses, eventIds: targetEventIds } = collectEngagementTargets(
indexEvents,
buildIndexByAddress(indexEvents)
)
if (import.meta.env.DEV) {
logger.info('[Library] fetching engagement', {
targetAddresses: targetAddresses.size,

11
src/pages/primary/LibraryPage/index.tsx

@ -20,13 +20,13 @@ const LibraryPage = forwardRef<TPageRef>((_props, ref) => { @@ -20,13 +20,13 @@ const LibraryPage = forwardRef<TPageRef>((_props, ref) => {
entries,
searchQuery,
setSearchQuery,
committedSearch,
searchAxis,
setSearchAxis,
commitSearch,
showOnlyMine,
setShowOnlyMine,
mineFilterLoading,
loading,
engagementLoading,
searchLoading,
relaySearchLoading,
error,
@ -70,8 +70,9 @@ const LibraryPage = forwardRef<TPageRef>((_props, ref) => { @@ -70,8 +70,9 @@ const LibraryPage = forwardRef<TPageRef>((_props, ref) => {
<LibrarySearchBar
searchQuery={searchQuery}
onSearchQueryChange={setSearchQuery}
committedSearch={committedSearch}
searchAxis={searchAxis}
onSearchAxisChange={setSearchAxis}
onCommitSearch={commitSearch}
showOnlyMine={showOnlyMine}
onShowOnlyMineChange={setShowOnlyMine}
mineFilterLoading={mineFilterLoading}
@ -87,8 +88,6 @@ const LibraryPage = forwardRef<TPageRef>((_props, ref) => { @@ -87,8 +88,6 @@ const LibraryPage = forwardRef<TPageRef>((_props, ref) => {
) : null}
{loading ? (
<p className="mb-4 text-xs text-muted-foreground">{t('Library loading')}</p>
) : engagementLoading ? (
<p className="mb-4 text-xs text-muted-foreground">{t('Library engagement loading')}</p>
) : searchLoading ? (
<p className="mb-4 text-xs text-muted-foreground">{t('Library search loading')}</p>
) : mineFilterLoading ? (
@ -106,7 +105,7 @@ const LibraryPage = forwardRef<TPageRef>((_props, ref) => { @@ -106,7 +105,7 @@ const LibraryPage = forwardRef<TPageRef>((_props, ref) => {
(showOnlyMine && mineFilterLoading)
}
emptyMessage={
searchQuery.trim() || showOnlyMine ? t('Library empty filtered') : t('Library empty')
committedSearch.trim() || showOnlyMine ? t('Library empty filtered') : t('Library empty')
}
/>
{defaultFeedHasMore ? (

Loading…
Cancel
Save