Browse Source

make profile feed more performant

imwald
Silberengel 4 months ago
parent
commit
6c622f870e
  1. 24
      src/components/Profile/ProfileArticles.tsx
  2. 14
      src/components/Profile/ProfileBookmarksAndHashtags.tsx
  3. 25
      src/components/Profile/ProfileFeed.tsx
  4. 68
      src/services/client.service.ts

24
src/components/Profile/ProfileArticles.tsx

@ -101,6 +101,17 @@ const ProfileArticles = forwardRef<{ refresh: () => void; getEvents: () => Event @@ -101,6 +101,17 @@ const ProfileArticles = forwardRef<{ refresh: () => void; getEvents: () => Event
// Sort by creation time (newest first)
eventsToShow.sort((a, b) => b.created_at - a.created_at)
// If initial load returns 0 events but it's not a retry, wait and retry once
// This handles cases where relays return "too many concurrent REQS" and return empty results
if (!isRetry && !isRefresh && eventsToShow.length === 0 && retryCount === 0) {
console.log('[ProfileArticles] Got 0 events on initial load, retrying after delay...')
setTimeout(() => {
setRetryCount(prev => prev + 1)
fetchArticles(true)
}, 2000) // Wait 2 seconds before retry to let relays recover
return
}
if (isRefresh) {
// For refresh, append new events and deduplicate
setEvents(prevEvents => {
@ -188,16 +199,17 @@ const ProfileArticles = forwardRef<{ refresh: () => void; getEvents: () => Event @@ -188,16 +199,17 @@ const ProfileArticles = forwardRef<{ refresh: () => void; getEvents: () => Event
}, [events, searchQuery, kindFilter])
// Separate effect for initial fetch only with a small delay
// Separate effect for initial fetch only - delay slightly to avoid race conditions with other tabs
useEffect(() => {
if (pubkey) {
// Add a small delay to let the component fully mount and relays to be ready
const timer = setTimeout(() => {
// Small delay to stagger initial fetches across tabs and allow relay list cache to populate
const timeoutId = setTimeout(() => {
fetchArticles()
}, 500) // 500ms delay
return () => clearTimeout(timer)
}, 150) // 150ms delay (slightly longer than posts) to allow previous fetches to populate cache
return () => clearTimeout(timeoutId)
}
}, [pubkey]) // Only depend on pubkey to avoid loops
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pubkey]) // Only depend on pubkey - fetchArticles is stable from useCallback
if (isLoading || isRetrying) {
return (

14
src/components/Profile/ProfileBookmarksAndHashtags.tsx

@ -378,19 +378,19 @@ const ProfileBookmarksAndHashtags = forwardRef<{ refresh: () => void }, { @@ -378,19 +378,19 @@ const ProfileBookmarksAndHashtags = forwardRef<{ refresh: () => void }, {
refresh
}), [refresh])
// Fetch data when component mounts or pubkey changes with a small delay
// Fetch data when component mounts or pubkey changes - delay slightly to avoid race conditions
useEffect(() => {
if (pubkey) {
// Add a small delay to let the component fully mount and relays to be ready
const timer = setTimeout(() => {
// Small delay to stagger initial fetches and allow relay list cache to populate
const timeoutId = setTimeout(() => {
fetchBookmarks()
fetchHashtags()
fetchPins()
}, 500) // 500ms delay
return () => clearTimeout(timer)
}, 200) // 200ms delay (longest since this component does 3 fetches) to allow previous fetches to populate cache
return () => clearTimeout(timeoutId)
}
}, [pubkey]) // Only depend on pubkey to avoid loops
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pubkey]) // Only depend on pubkey - fetch functions are stable from useCallback
// Check if the requested tab has content
const hasContent = useMemo(() => {

25
src/components/Profile/ProfileFeed.tsx

@ -99,6 +99,17 @@ const ProfileFeed = forwardRef<{ refresh: () => void }, ProfileFeedProps>(({ pub @@ -99,6 +99,17 @@ const ProfileFeed = forwardRef<{ refresh: () => void }, ProfileFeedProps>(({ pub
// Sort by creation time (newest first)
eventsToShow.sort((a, b) => b.created_at - a.created_at)
// If initial load returns 0 events but it's not a retry, wait and retry once
// This handles cases where relays return "too many concurrent REQS" and return empty results
if (!isRetry && !isRefresh && eventsToShow.length === 0 && retryCount === 0) {
console.log('[ProfileFeed] Got 0 events on initial load, retrying after delay...')
setTimeout(() => {
setRetryCount(prev => prev + 1)
fetchPosts(true)
}, 2000) // Wait 2 seconds before retry to let relays recover
return
}
if (isRefresh) {
// For refresh, append new events and deduplicate
setEvents(prevEvents => {
@ -166,17 +177,17 @@ const ProfileFeed = forwardRef<{ refresh: () => void }, ProfileFeedProps>(({ pub @@ -166,17 +177,17 @@ const ProfileFeed = forwardRef<{ refresh: () => void }, ProfileFeedProps>(({ pub
)
}, [events, searchQuery])
// Separate effect for initial fetch only with a small delay
// Separate effect for initial fetch only - delay slightly to avoid race conditions with other tabs
useEffect(() => {
if (pubkey) {
// Add a small delay to let the component fully mount and relays to be ready
const timer = setTimeout(() => {
// Small delay to stagger initial fetches across tabs and allow relay list cache to populate
const timeoutId = setTimeout(() => {
fetchPosts()
}, 500) // 500ms delay
return () => clearTimeout(timer)
}, 100) // 100ms delay to allow previous fetches to populate cache
return () => clearTimeout(timeoutId)
}
}, [pubkey]) // Only depend on pubkey to avoid loops
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pubkey]) // Only depend on pubkey - fetchPosts is stable from useCallback
if (isLoading || isRetrying) {
return (

68
src/services/client.service.ts

@ -52,6 +52,7 @@ class ClientService extends EventTarget { @@ -52,6 +52,7 @@ class ClientService extends EventTarget {
> = {}
private replaceableEventCacheMap = new Map<string, NEvent>()
private eventCacheMap = new Map<string, Promise<NEvent | undefined>>()
private relayListRequestCache = new Map<string, Promise<TRelayList>>() // Cache in-flight relay list requests
private eventDataLoader = new DataLoader<string, NEvent | undefined>(
(ids) => Promise.all(ids.map((id) => this._fetchEvent(id))),
{ cacheMap: this.eventCacheMap }
@ -755,21 +756,62 @@ class ClientService extends EventTarget { @@ -755,21 +756,62 @@ class ClientService extends EventTarget {
private async query(urls: string[], filter: Filter | Filter[], onevent?: (evt: NEvent) => void) {
return await new Promise<NEvent[]>((resolve) => {
const events: NEvent[] = []
let hasEosed = false
let resolveTimeout: ReturnType<typeof setTimeout> | null = null
const resolveWithEvents = () => {
if (resolveTimeout) {
clearTimeout(resolveTimeout)
resolveTimeout = null
}
sub.close()
resolve(events)
}
const sub = this.subscribe(urls, filter, {
onevent(evt) {
onevent?.(evt)
events.push(evt)
// If we got events, clear any timeout - we're making progress
if (resolveTimeout) {
clearTimeout(resolveTimeout)
resolveTimeout = null
}
},
oneose: (eosed) => {
if (eosed) {
sub.close()
resolve(events)
hasEosed = true
// Wait a bit more after EOSE to ensure we got all events
resolveTimeout = setTimeout(() => {
resolveWithEvents()
}, 500)
}
},
onclose: () => {
resolve(events)
// Only resolve immediately on close if we've received EOSE or have events
// Otherwise, wait a bit to see if more events come
if (hasEosed || events.length > 0) {
if (resolveTimeout) {
clearTimeout(resolveTimeout)
}
resolve(events)
} else {
// Wait up to 3 seconds for events if connection closes early
resolveTimeout = setTimeout(() => {
resolve(events)
}, 3000)
}
}
})
// Fallback timeout: resolve after 10 seconds max to prevent hanging
setTimeout(() => {
if (resolveTimeout) {
clearTimeout(resolveTimeout)
}
sub.close()
resolve(events)
}, 10000)
})
}
@ -1240,8 +1282,24 @@ class ClientService extends EventTarget { @@ -1240,8 +1282,24 @@ class ClientService extends EventTarget {
}
async fetchRelayList(pubkey: string): Promise<TRelayList> {
const [relayList] = await this.fetchRelayLists([pubkey])
return relayList
// Deduplicate concurrent requests for the same pubkey's relay list
const existingRequest = this.relayListRequestCache.get(pubkey)
if (existingRequest) {
return existingRequest
}
const requestPromise = (async () => {
try {
const [relayList] = await this.fetchRelayLists([pubkey])
return relayList
} finally {
// Remove from cache after completion (cache result in replaceableEventCacheMap)
this.relayListRequestCache.delete(pubkey)
}
})()
this.relayListRequestCache.set(pubkey, requestPromise)
return requestPromise
}
async fetchRelayLists(pubkeys: string[]): Promise<TRelayList[]> {

Loading…
Cancel
Save