Browse Source

bug-fix

imwald
Silberengel 1 month ago
parent
commit
a925184e65
  1. 47
      src/hooks/useFetchProfile.tsx
  2. 23
      src/lib/relay-list-builder.ts
  3. 90
      src/providers/NotificationProvider.tsx
  4. 2
      src/services/client-query.service.ts
  5. 170
      src/services/client-replaceable-events.service.ts

47
src/hooks/useFetchProfile.tsx

@ -126,9 +126,15 @@ export function useFetchProfile(id?: string, skipCache = false) {
// CRITICAL: Early exit if already processing this exact pubkey - prevents infinite loops // CRITICAL: Early exit if already processing this exact pubkey - prevents infinite loops
// This check must happen FIRST, before any other logic // This check must happen FIRST, before any other logic
if (extractedPubkey && processingPubkeyRef.current === extractedPubkey) { // Set processingPubkeyRef IMMEDIATELY after extraction to prevent race conditions
// Silently exit - no logging to reduce noise if (extractedPubkey) {
return if (processingPubkeyRef.current === extractedPubkey) {
// Silently exit - no logging to reduce noise
return
}
// Mark that we're processing this pubkey IMMEDIATELY to prevent concurrent runs
// We'll clear it later if we early exit for other reasons
processingPubkeyRef.current = extractedPubkey
} }
// CRITICAL: Early exit if we already have a profile for this pubkey // CRITICAL: Early exit if we already have a profile for this pubkey
@ -152,13 +158,20 @@ export function useFetchProfile(id?: string, skipCache = false) {
// CRITICAL: Early exit if we've already initialized this pubkey (even if profile is null) // CRITICAL: Early exit if we've already initialized this pubkey (even if profile is null)
// This prevents re-fetching when we've already tried and failed // This prevents re-fetching when we've already tried and failed
// BUT: Allow retry if skipCache is true (user explicitly wants to refresh) // BUT: Allow retry if skipCache is true (user explicitly wants to refresh)
if (extractedPubkey && initializedPubkeysRef.current.has(extractedPubkey) && !profile && !skipCache) { if (extractedPubkey && initializedPubkeysRef.current.has(extractedPubkey) && !profile) {
// Already tried and failed - don't retry unless explicitly requested if (skipCache) {
// Ensure fetching is false // User wants to refresh - clear initialized flag to allow fresh fetch
if (isFetching) { initializedPubkeysRef.current.delete(extractedPubkey)
setIsFetching(false) // Also clear run count to allow fresh attempt
effectRunCountRef.current.delete(extractedPubkey)
} else {
// Already tried and failed - don't retry unless explicitly requested
// Ensure fetching is false
if (isFetching) {
setIsFetching(false)
}
return
} }
return
} }
// CRITICAL: Guard against infinite loops - limit effect runs per pubkey (reduced from 10 to 3) // CRITICAL: Guard against infinite loops - limit effect runs per pubkey (reduced from 10 to 3)
@ -247,29 +260,27 @@ export function useFetchProfile(id?: string, skipCache = false) {
} }
// These checks are now done earlier in the effect (before incrementing run count) // These checks are now done earlier in the effect (before incrementing run count)
// Keeping this as a safety check, but it should rarely be hit // Keeping this as a safety check, but it should rarely be hit now that we set processingPubkeyRef earlier
if (processingPubkeyRef.current === extractedPubkey) { if (processingPubkeyRef.current !== extractedPubkey) {
logger.info('[useFetchProfile] Already processing this pubkey (safety check)', { // This should never happen now, but keep as safety check
logger.warn('[useFetchProfile] processingPubkeyRef mismatch (safety check)', {
extractedPubkey, extractedPubkey,
processingPubkey: processingPubkeyRef.current processingPubkey: processingPubkeyRef.current
}) })
return processingPubkeyRef.current = extractedPubkey
} }
if (profile && profile.pubkey === extractedPubkey) { if (profile && profile.pubkey === extractedPubkey) {
logger.info('[useFetchProfile] Already have profile for this pubkey (safety check)', { logger.info('[useFetchProfile] Already have profile for this pubkey (safety check)', {
extractedPubkey extractedPubkey
}) })
processingPubkeyRef.current = extractedPubkey
setIsFetching(false) setIsFetching(false)
effectRunCountRef.current.delete(extractedPubkey) effectRunCountRef.current.delete(extractedPubkey)
return return
} }
// CRITICAL: Mark that we're processing this pubkey IMMEDIATELY after validation // processingPubkeyRef is already set earlier (right after extraction)
// This must happen before any state updates or async operations // No need to set it again here
// This prevents the effect from running again for the same pubkey
processingPubkeyRef.current = extractedPubkey
// CRITICAL: Only update pubkey state if it's actually different // CRITICAL: Only update pubkey state if it's actually different
// Avoid state updates that could trigger re-renders and loops // Avoid state updates that could trigger re-renders and loops

23
src/lib/relay-list-builder.ts

@ -40,6 +40,8 @@ export interface RelayListBuilderOptions {
blockedRelays?: string[] blockedRelays?: string[]
/** Whether to include local relays from kind 10432 */ /** Whether to include local relays from kind 10432 */
includeLocalRelays?: boolean includeLocalRelays?: boolean
/** Whether to include user's favorite relays (kind 10012) */
includeFavoriteRelays?: boolean
} }
/** /**
@ -58,7 +60,8 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio
includeFastWriteRelays = false, includeFastWriteRelays = false,
includeSearchableRelays = false, includeSearchableRelays = false,
blockedRelays = [], blockedRelays = [],
includeLocalRelays = true includeLocalRelays = true,
includeFavoriteRelays = false
} = options } = options
const relayUrls = new Set<string>() const relayUrls = new Set<string>()
@ -150,10 +153,26 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio
localRelays.forEach(addRelay) localRelays.forEach(addRelay)
} }
// Include favorite relays (kind 10012) if requested
let favoriteRelaysCount = 0
if (includeFavoriteRelays) {
try {
const favoriteRelays = await client.fetchFavoriteRelays(userPubkey)
favoriteRelays.forEach(addRelay)
favoriteRelaysCount = favoriteRelays.length
logger.debug('[RelayListBuilder] Added user favorite relays', {
count: favoriteRelaysCount
})
} catch (error) {
logger.debug('[RelayListBuilder] Failed to fetch user favorite relays', { error })
}
}
logger.debug('[RelayListBuilder] Added user own relays', { logger.debug('[RelayListBuilder] Added user own relays', {
read: userRelayList ? (userRelayList.read || []).length : 0, read: userRelayList ? (userRelayList.read || []).length : 0,
write: userRelayList ? (userRelayList.write || []).length : 0, write: userRelayList ? (userRelayList.write || []).length : 0,
local: includeLocalRelays ? (await getCacheRelayUrls(userPubkey)).length : 0 local: includeLocalRelays ? (await getCacheRelayUrls(userPubkey)).length : 0,
favorite: favoriteRelaysCount
}) })
} catch (error) { } catch (error) {
logger.debug('[RelayListBuilder] Failed to fetch user relay list', { error }) logger.debug('[RelayListBuilder] Failed to fetch user relay list', { error })

90
src/providers/NotificationProvider.tsx

@ -5,7 +5,7 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import client from '@/services/client.service' import client from '@/services/client.service'
import { kinds, NostrEvent } from 'nostr-tools' import { kinds, NostrEvent } from 'nostr-tools'
import { SubCloser } from 'nostr-tools/abstract-pool' import { SubCloser } from 'nostr-tools/abstract-pool'
import { useEffect, useRef } from 'react' import { useEffect, useRef, useMemo } from 'react'
import { useNostr } from './NostrProvider' import { useNostr } from './NostrProvider'
/** /**
@ -16,6 +16,30 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
const { pubkey, relayList } = useNostr() const { pubkey, relayList } = useNostr()
const { favoriteRelays } = useFavoriteRelays() const { favoriteRelays } = useFavoriteRelays()
const notificationBufferRef = useRef<NostrEvent[]>([]) const notificationBufferRef = useRef<NostrEvent[]>([])
const retryCountRef = useRef(0)
const retryTimeoutIdRef = useRef<NodeJS.Timeout | null>(null)
// Memoize relay URLs to prevent unnecessary re-subscriptions
// This creates stable references based on actual relay URLs, not object references
const userReadRelays = useMemo(() => {
const userRelayList = relayList || { read: [], write: [] }
return userRelayList.read || []
}, [relayList?.read?.join(',')]) // Compare by stringified array, not object reference
const userFavoriteRelays = useMemo(() => {
return favoriteRelays || []
}, [favoriteRelays?.join(',')]) // Compare by stringified array, not array reference
// Memoize the notification relays to prevent re-subscriptions when they haven't changed
const notificationRelays = useMemo(() => {
if (userReadRelays.length > 0) {
return userReadRelays.slice(0, 5)
} else if (userFavoriteRelays.length > 0) {
return userFavoriteRelays.slice(0, 5)
} else {
return FAST_READ_RELAY_URLS.slice(0, 5)
}
}, [userReadRelays, userFavoriteRelays])
useEffect(() => { useEffect(() => {
if (!pubkey) return if (!pubkey) return
@ -31,8 +55,17 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
const topicSubCloserRef: { const topicSubCloserRef: {
current: SubCloser | null current: SubCloser | null
} = { current: null } } = { current: null }
const MAX_RETRIES = 5
// Reset retry count when effect runs (relays changed)
retryCountRef.current = 0
const subscribe = async () => { const subscribe = async () => {
// Clear any pending retries
if (retryTimeoutIdRef.current) {
clearTimeout(retryTimeoutIdRef.current)
retryTimeoutIdRef.current = null
}
if (subCloserRef.current) { if (subCloserRef.current) {
subCloserRef.current.close() subCloserRef.current.close()
subCloserRef.current = null subCloserRef.current = null
@ -45,27 +78,11 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
try { try {
let eosed = false let eosed = false
const userRelayList = relayList || { read: [], write: [] } // Reset retry count on successful subscription attempt
const userReadRelays = userRelayList.read || [] retryCountRef.current = 0
const userFavoriteRelays = favoriteRelays || []
let notificationRelays: string[] = [] if (notificationRelays.length > 0) {
logger.component('NotificationProvider', 'Using notification relays', {
if (userReadRelays.length > 0) {
notificationRelays = userReadRelays.slice(0, 5)
logger.component('NotificationProvider', 'Using user read relays', {
count: notificationRelays.length,
relays: notificationRelays.slice(0, 3)
})
} else if (userFavoriteRelays.length > 0) {
notificationRelays = userFavoriteRelays.slice(0, 5)
logger.component('NotificationProvider', 'Using user favorite relays', {
count: notificationRelays.length,
relays: notificationRelays.slice(0, 3)
})
} else {
notificationRelays = FAST_READ_RELAY_URLS.slice(0, 5)
logger.component('NotificationProvider', 'Using fast read relays fallback', {
count: notificationRelays.length, count: notificationRelays.length,
relays: notificationRelays.slice(0, 3) relays: notificationRelays.slice(0, 3)
}) })
@ -153,13 +170,17 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
return return
} }
if (isMountedRef.current) { if (isMountedRef.current && retryCountRef.current < MAX_RETRIES) {
setTimeout(() => { retryCountRef.current++
const delay = Math.min(15_000 * retryCountRef.current, 60_000) // Exponential backoff, max 60s
logger.debug(`[NotificationProvider] Reconnecting after close (attempt ${retryCountRef.current}/${MAX_RETRIES})...`)
retryTimeoutIdRef.current = setTimeout(() => {
if (isMountedRef.current) { if (isMountedRef.current) {
logger.debug('[NotificationProvider] Reconnecting after close...')
subscribe() subscribe()
} }
}, 15_000) }, delay)
} else if (retryCountRef.current >= MAX_RETRIES) {
logger.error('[NotificationProvider] Max retries reached, stopping reconnection attempts')
} }
} }
} }
@ -168,14 +189,18 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
subCloserRef.current = subCloser subCloserRef.current = subCloser
return subCloser return subCloser
} catch (error) { } catch (error) {
logger.error('Subscription error', { error }) logger.error('Subscription error', { error, retryCount: retryCountRef.current })
if (isMountedRef.current) { if (isMountedRef.current && retryCountRef.current < MAX_RETRIES) {
setTimeout(() => { retryCountRef.current++
const delay = Math.min(5_000 * retryCountRef.current, 30_000) // Exponential backoff, max 30s
retryTimeoutIdRef.current = setTimeout(() => {
if (isMountedRef.current) { if (isMountedRef.current) {
subscribe() subscribe()
} }
}, 5_000) }, delay)
} else if (retryCountRef.current >= MAX_RETRIES) {
logger.error('[NotificationProvider] Max retries reached, stopping subscription attempts')
} }
return null return null
} }
@ -185,6 +210,11 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
return () => { return () => {
clearTimeout(deferredReset) clearTimeout(deferredReset)
if (retryTimeoutIdRef.current) {
clearTimeout(retryTimeoutIdRef.current)
retryTimeoutIdRef.current = null
}
retryCountRef.current = 0 // Reset retry count on cleanup
isMountedRef.current = false isMountedRef.current = false
if (subCloserRef.current) { if (subCloserRef.current) {
subCloserRef.current.close() subCloserRef.current.close()
@ -195,7 +225,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
topicSubCloserRef.current = null topicSubCloserRef.current = null
} }
} }
}, [pubkey, relayList, favoriteRelays]) }, [pubkey, notificationRelays.join(',')]) // Use memoized notificationRelays instead of relayList/favoriteRelays
return <>{children}</> return <>{children}</>
} }

2
src/services/client-query.service.ts

@ -129,7 +129,7 @@ export class QueryService {
} }
const FIRST_RESULT_GRACE_MS = 1200 const FIRST_RESULT_GRACE_MS = 1200
const REPLACEABLE_RACE_WAIT_MS = 2000 const REPLACEABLE_RACE_WAIT_MS = 1000 // Reduced from 2000ms for faster profile loading in feeds
return await new Promise<NEvent[]>((resolve) => { return await new Promise<NEvent[]>((resolve) => {
const events: NEvent[] = [] const events: NEvent[] = []

170
src/services/client-replaceable-events.service.ts

@ -160,8 +160,8 @@ export class ReplaceableEventService {
kinds: [kind] kinds: [kind]
}, undefined, { }, undefined, {
replaceableRace: true, replaceableRace: true,
eoseTimeout: 200, eoseTimeout: 100, // Reduced from 200ms for faster early returns
globalTimeout: 3000 globalTimeout: 2000 // Reduced from 3000ms to prevent long waits when many relays are slow
}) })
const queryTime = Date.now() - startTime const queryTime = Date.now() - startTime
logger.info('[ReplaceableEventService] Query completed', { logger.info('[ReplaceableEventService] Query completed', {
@ -438,8 +438,8 @@ export class ReplaceableEventService {
kinds: [kind] kinds: [kind]
}, undefined, { }, undefined, {
replaceableRace: true, replaceableRace: true,
eoseTimeout: 200, eoseTimeout: 100, // Reduced from 200ms for faster early returns
globalTimeout: 3000 globalTimeout: 2000 // Reduced from 3000ms to prevent long waits when many relays are slow
}) })
logger.info('[ReplaceableEventService] Query completed for batch', { logger.info('[ReplaceableEventService] Query completed for batch', {
kind, kind,
@ -526,8 +526,8 @@ export class ReplaceableEventService {
const events = await this.queryService.query(relayUrls, filter, undefined, { const events = await this.queryService.query(relayUrls, filter, undefined, {
replaceableRace: true, replaceableRace: true,
eoseTimeout: 200, eoseTimeout: 100, // Reduced from 200ms for faster early returns
globalTimeout: 3000 globalTimeout: 2000 // Reduced from 3000ms to prevent long waits when many relays are slow
}) })
for (const event of events) { for (const event of events) {
@ -719,68 +719,79 @@ export class ReplaceableEventService {
} }
// Step 3: Comprehensive search across ALL available relays before giving up // Step 3: Comprehensive search across ALL available relays before giving up
// This includes: local relays, user inboxes/outboxes, fast read/write, searchable relays // OPTIMIZATION: Skip comprehensive search for batch profile fetches (when called from DataLoader)
logger.info('[ReplaceableEventService] Step 3: Profile not found, trying comprehensive relay list (all available relays)', { // Comprehensive search is expensive (10s timeout) and should only be used for individual profile fetches
pubkey // when user explicitly navigates to a profile page. For feed rendering, missing profiles are acceptable.
}) // Only run comprehensive search if we have relay hints (suggesting user intent to find this specific profile)
if (relayHints.length > 0) {
try { logger.info('[ReplaceableEventService] Step 3: Profile not found, trying comprehensive relay list (all available relays)', {
const userPubkey = client.pubkey
const comprehensiveRelays = await buildComprehensiveRelayList({
authorPubkey: pubkey,
userPubkey: userPubkey || undefined,
relayHints: relayHints.length > 0 ? relayHints : undefined,
includeUserOwnRelays: true, // Include user's read/write relays
includeProfileFetchRelays: true, // Include PROFILE_FETCH_RELAY_URLS
includeFastReadRelays: true, // Include FAST_READ_RELAY_URLS
includeFastWriteRelays: true, // Include FAST_WRITE_RELAY_URLS
includeSearchableRelays: true, // Include SEARCHABLE_RELAY_URLS
includeLocalRelays: true // Include local/cache relays
})
logger.info('[ReplaceableEventService] Comprehensive relay list built', {
pubkey, pubkey,
relayCount: comprehensiveRelays.length, hasRelayHints: relayHints.length > 0
relays: comprehensiveRelays.slice(0, 10) // Log first 10 for debugging
}) })
if (comprehensiveRelays.length > 0) { try {
// Query the comprehensive relay list const userPubkey = client.pubkey
const startTime = Date.now() const comprehensiveRelays = await buildComprehensiveRelayList({
const events = await this.queryService.query(comprehensiveRelays, { authorPubkey: pubkey,
authors: [pubkey], userPubkey: userPubkey || undefined,
kinds: [kinds.Metadata] relayHints: relayHints.length > 0 ? relayHints : undefined,
}, undefined, { includeUserOwnRelays: true, // Include user's read/write relays
replaceableRace: true, includeFavoriteRelays: true, // Include user's favorite relays (kind 10012)
eoseTimeout: 500, includeProfileFetchRelays: true, // Include PROFILE_FETCH_RELAY_URLS
globalTimeout: 10000 // 10 second timeout for comprehensive search includeFastReadRelays: true, // Include FAST_READ_RELAY_URLS
includeFastWriteRelays: true, // Include FAST_WRITE_RELAY_URLS
includeSearchableRelays: true, // Include SEARCHABLE_RELAY_URLS
includeLocalRelays: true // Include local/cache relays
}) })
const queryTime = Date.now() - startTime
logger.info('[ReplaceableEventService] Comprehensive search completed', { logger.info('[ReplaceableEventService] Comprehensive relay list built', {
pubkey, pubkey,
eventCount: events.length, relayCount: comprehensiveRelays.length,
queryTime: `${queryTime}ms`, relays: comprehensiveRelays.slice(0, 10) // Log first 10 for debugging
relayCount: comprehensiveRelays.length
}) })
if (events.length > 0) { if (comprehensiveRelays.length > 0) {
const sortedEvents = events.sort((a, b) => b.created_at - a.created_at) // Query the comprehensive relay list with reduced timeout for faster failure
const profileEvent = sortedEvents[0] const startTime = Date.now()
logger.info('[ReplaceableEventService] Profile found via comprehensive search', { const events = await this.queryService.query(comprehensiveRelays, {
authors: [pubkey],
kinds: [kinds.Metadata]
}, undefined, {
replaceableRace: true,
eoseTimeout: 300, // Reduced from 500ms
globalTimeout: 5000 // Reduced from 10000ms to prevent 10s waits
})
const queryTime = Date.now() - startTime
logger.info('[ReplaceableEventService] Comprehensive search completed', {
pubkey, pubkey,
eventId: profileEvent.id eventCount: events.length,
queryTime: `${queryTime}ms`,
relayCount: comprehensiveRelays.length
}) })
await this.indexProfile(profileEvent)
return profileEvent if (events.length > 0) {
const sortedEvents = events.sort((a, b) => b.created_at - a.created_at)
const profileEvent = sortedEvents[0]
logger.info('[ReplaceableEventService] Profile found via comprehensive search', {
pubkey,
eventId: profileEvent.id
})
await this.indexProfile(profileEvent)
return profileEvent
}
} }
} catch (error) {
logger.error('[ReplaceableEventService] Comprehensive search failed', {
pubkey,
error: error instanceof Error ? error.message : String(error)
})
// Continue to return undefined below
} }
} catch (error) { } else {
logger.error('[ReplaceableEventService] Comprehensive search failed', { logger.debug('[ReplaceableEventService] Skipping comprehensive search (no relay hints, likely batch fetch)', {
pubkey, pubkey
error: error instanceof Error ? error.message : String(error)
}) })
// Continue to return undefined below
} }
logger.warn('[ReplaceableEventService] Profile not found after trying all relays (including comprehensive search)', { logger.warn('[ReplaceableEventService] Profile not found after trying all relays (including comprehensive search)', {
@ -954,10 +965,12 @@ export class ReplaceableEventService {
/** /**
* Fetch following favorite relays * Fetch following favorite relays
*/ */
async fetchFollowingFavoriteRelays(pubkey: string): Promise<[string, string[]][]> { async fetchFollowingFavoriteRelays(pubkey: string, skipCache = false): Promise<[string, string[]][]> {
const cached = this.followingFavoriteRelaysCache.get(pubkey) if (!skipCache) {
if (cached) { const cached = this.followingFavoriteRelaysCache.get(pubkey)
return cached if (cached) {
return cached
}
} }
const promise = this._fetchFollowingFavoriteRelays(pubkey) const promise = this._fetchFollowingFavoriteRelays(pubkey)
this.followingFavoriteRelaysCache.set(pubkey, promise) this.followingFavoriteRelaysCache.set(pubkey, promise)
@ -966,28 +979,47 @@ export class ReplaceableEventService {
private async _fetchFollowingFavoriteRelays(pubkey: string): Promise<[string, string[]][]> { private async _fetchFollowingFavoriteRelays(pubkey: string): Promise<[string, string[]][]> {
const followings = await this.fetchFollowings(pubkey) const followings = await this.fetchFollowings(pubkey)
const followingsToProcess = followings.slice(0, 100)
const favoriteRelaysEvents = await this.fetchReplaceableEventsFromProfileFetchRelays( const favoriteRelaysEvents = await this.fetchReplaceableEventsFromProfileFetchRelays(
followings.slice(0, 100), followingsToProcess,
ExtendedKind.FAVORITE_RELAYS ExtendedKind.FAVORITE_RELAYS
) )
const result: [string, string[]][] = [] // Group by relay URL: Map<relayUrl, Set<pubkey>>
for (let i = 0; i < followings.length && i < favoriteRelaysEvents.length; i++) { const relayToUsers = new Map<string, Set<string>>()
// favoriteRelaysEvents[i] corresponds to followingsToProcess[i]
for (let i = 0; i < followingsToProcess.length && i < favoriteRelaysEvents.length; i++) {
const event = favoriteRelaysEvents[i] const event = favoriteRelaysEvents[i]
if (event) { const followingPubkey = followingsToProcess[i]
const relays: string[] = [] if (event && followingPubkey) {
event.tags.forEach(([tagName, tagValue]) => { event.tags.forEach(([tagName, tagValue]) => {
if (tagName === 'relay' && tagValue) { if (tagName === 'relay' && tagValue) {
const normalizedUrl = normalizeUrl(tagValue) const normalizedUrl = normalizeUrl(tagValue)
if (normalizedUrl && !relays.includes(normalizedUrl)) { if (normalizedUrl) {
relays.push(normalizedUrl) if (!relayToUsers.has(normalizedUrl)) {
relayToUsers.set(normalizedUrl, new Set())
}
relayToUsers.get(normalizedUrl)!.add(followingPubkey)
} }
} }
}) })
if (relays.length > 0) {
result.push([followings[i]!, relays])
}
} }
} }
// Convert to array format: [relayUrl, pubkeys[]]
const result: [string, string[]][] = []
for (const [relayUrl, pubkeys] of relayToUsers.entries()) {
result.push([relayUrl, Array.from(pubkeys)])
}
logger.debug('[ReplaceableEventService] fetchFollowingFavoriteRelays completed', {
followingsCount: followings.length,
processedCount: followingsToProcess.length,
eventsFound: favoriteRelaysEvents.filter(e => e !== undefined).length,
uniqueRelays: result.length,
totalUsers: result.reduce((sum, [, users]) => sum + users.length, 0)
})
return result return result
} }
} }

Loading…
Cancel
Save