+
{t('Full-text search merged intro', {
relayCount: normalizedRelays.length,
seconds: timeoutSec,
@@ -332,42 +493,52 @@ export default function FullTextSearchByRelay({
)}
-
- {anyLoading && mergedHits.length === 0 && (
-
-
-
-
-
- )}
-
- {mergedHits.map((hit) => (
-
-
+
+
+ {anyLoading && mergedHits.length === 0 && (
+
+
+
+
+
+ )}
+
+ {mergedHits.map((hit) => (
+
-
+
{t('Full-text search seen on label')}
- {hit.relayUrls.map((url) => (
-
-
-
- ))}
+
+ {hit.relayUrls.map((url) => (
+
+
+
+ ))}
+
-
-
-
-
-
- ))}
-
+
+
+ ))}
+
+
{allTerminal && mergedHits.length === 0 && (
diff --git a/src/lib/relay-strikes.ts b/src/lib/relay-strikes.ts
index 5284b899..c9e28dbe 100644
--- a/src/lib/relay-strikes.ts
+++ b/src/lib/relay-strikes.ts
@@ -65,6 +65,8 @@ function sessionKey(url: string): string {
class RelaySessionStrikes {
private byKey = new Map()
private cacheRelayKeys = new Set()
+ /** Throttle debug spam when many parallel REQs hit the same dead relay (cache rows bypass strike debounce). */
+ private lastReadFailureDebugLogAt = new Map()
setSessionCacheRelayKeysFromKind10432(ev: Event | null | undefined): void {
this.cacheRelayKeys.clear()
@@ -168,7 +170,11 @@ class RelaySessionStrikes {
e.readStrikeSkipUntil = Math.max(e.readStrikeSkipUntil, now + STRIKE_COOLDOWN_MS)
logger.info('[RelayStrikes] read path strike skip', { key, readFailures: e.readFailures })
} else {
- logger.debug('[RelayStrikes] read failure counted', { key, readFailures: e.readFailures, cache })
+ const lastDbg = this.lastReadFailureDebugLogAt.get(key) ?? 0
+ if (now - lastDbg >= STRIKE_INCREMENT_DEBOUNCE_MS) {
+ this.lastReadFailureDebugLogAt.set(key, now)
+ logger.debug('[RelayStrikes] read failure counted', { key, readFailures: e.readFailures, cache })
+ }
}
}
@@ -180,6 +186,7 @@ class RelaySessionStrikes {
e.readFailures = 0
e.readStrikeSkipUntil = 0
e.readLastStrikeIncrementAt = 0
+ this.lastReadFailureDebugLogAt.delete(key)
}
recordPublishFailure(url: string): void {
@@ -241,6 +248,7 @@ class RelaySessionStrikes {
reset(): void {
this.byKey.clear()
this.cacheRelayKeys.clear()
+ this.lastReadFailureDebugLogAt.clear()
}
}
diff --git a/src/providers/FeedProvider.tsx b/src/providers/FeedProvider.tsx
index 6bc3d218..6cf25298 100644
--- a/src/providers/FeedProvider.tsx
+++ b/src/providers/FeedProvider.tsx
@@ -6,7 +6,7 @@ import logger from '@/lib/logger'
import { AGGR_NOSTR_LAND_WSS } from '@/lib/nostr-land-aggr'
import { normalizeAnyRelayUrl } from '@/lib/url'
import { buildWispTrendingNotesRelayUrl } from '@/lib/wisp-trending-relay'
-import { useEffect, useMemo, useState, useCallback } from 'react'
+import { useEffect, useMemo, useState, useCallback, useRef } from 'react'
import type { Dispatch, ReactNode, SetStateAction } from 'react'
import { FeedContext } from './feed-context'
import { useFavoriteRelays } from './FavoriteRelaysProvider'
@@ -117,6 +117,7 @@ export function FeedProvider({ children }: { children: ReactNode }) {
[]
)
+ const lastHomeFeedUrlLogRef = useRef({ primary: '', reply: '' })
const updateFeedRelayUrls = useCallback(() => {
const primaryRelays = buildAllFavoritesFeedRelayUrls(favoriteFeedRelayUrls, blockedRelays, primaryExtraRelayUrls)
const aggrEligibleRelayUrls = [
@@ -133,10 +134,16 @@ export function FeedProvider({ children }: { children: ReactNode }) {
relayListMentionsNostrLand(aggrEligibleRelayUrls),
blockedRelays
)
- logger.debug('Updating home feed relay URLs:', {
- primaryRelays,
- replyRelays
- })
+ const primaryId = relayUrlListIdentity(primaryRelays)
+ const replyId = relayUrlListIdentity(replyRelays)
+ const prevUrls = lastHomeFeedUrlLogRef.current
+ if (prevUrls.primary !== primaryId || prevUrls.reply !== replyId) {
+ lastHomeFeedUrlLogRef.current = { primary: primaryId, reply: replyId }
+ logger.debug('Updating home feed relay URLs:', {
+ primaryRelays,
+ replyRelays
+ })
+ }
setUrlStateIfChanged(setRelayUrls, primaryRelays)
setUrlStateIfChanged(setReplyRelayUrls, replyRelays)
}, [favoriteFeedRelayUrls, blockedRelays, primaryExtraRelayUrls, replyExtraRelayLayers, setUrlStateIfChanged])
@@ -173,20 +180,39 @@ export function FeedProvider({ children }: { children: ReactNode }) {
.join('|'),
[replyExtraRelayLayers]
)
+ const lastRelayInitDebugKey = useRef('')
+ const lastHadFavoriteRelaysRef = useRef(null)
useEffect(() => {
- logger.debug('FeedProvider relay init:', {
- isInitialized,
- favoriteRelays: favoriteRelays.length,
- relaySets: relaySets.length,
- relaySetRelays: favoriteFeedRelayUrls.length - favoriteRelays.length,
- inboxRelays: replyExtraRelayLayers.inboxRelayUrls.length,
- outboxRelays: replyExtraRelayLayers.outboxRelayUrls.length,
- cacheRelays: replyExtraRelayLayers.cacheRelayUrls.length,
- httpRelays: replyExtraRelayLayers.httpRelayUrls.length,
- blockedRelays: blockedRelays.length
- })
-
- if (favoriteFeedRelayUrls.length === 0) {
+ const initKey = [
+ isInitialized ? '1' : '0',
+ favoriteRelays.length,
+ relaySets.length,
+ favoriteFeedRelayUrls.length - favoriteRelays.length,
+ replyExtraRelayLayers.inboxRelayUrls.length,
+ replyExtraRelayLayers.outboxRelayUrls.length,
+ replyExtraRelayLayers.cacheRelayUrls.length,
+ replyExtraRelayLayers.httpRelayUrls.length,
+ blockedRelays.length
+ ].join('\x1e')
+ if (initKey !== lastRelayInitDebugKey.current) {
+ lastRelayInitDebugKey.current = initKey
+ logger.debug('FeedProvider relay init:', {
+ isInitialized,
+ favoriteRelays: favoriteRelays.length,
+ relaySets: relaySets.length,
+ relaySetRelays: favoriteFeedRelayUrls.length - favoriteRelays.length,
+ inboxRelays: replyExtraRelayLayers.inboxRelayUrls.length,
+ outboxRelays: replyExtraRelayLayers.outboxRelayUrls.length,
+ cacheRelays: replyExtraRelayLayers.cacheRelayUrls.length,
+ httpRelays: replyExtraRelayLayers.httpRelayUrls.length,
+ blockedRelays: blockedRelays.length
+ })
+ }
+
+ const hasFavoriteRelays = favoriteFeedRelayUrls.length > 0
+ const prevHad = lastHadFavoriteRelaysRef.current
+ lastHadFavoriteRelaysRef.current = hasFavoriteRelays
+ if (!hasFavoriteRelays && prevHad !== false) {
logger.debug('FeedProvider: no favorite or relay-set relays, using defaults')
}
diff --git a/src/providers/UserTrustProvider.tsx b/src/providers/UserTrustProvider.tsx
index 8f7d6ee4..7a4fda8b 100644
--- a/src/providers/UserTrustProvider.tsx
+++ b/src/providers/UserTrustProvider.tsx
@@ -1,6 +1,8 @@
+import { getPubkeysFromPTags } from '@/lib/tag'
import storage from '@/services/local-storage.service'
import { replaceableEventService } from '@/services/client.service'
import { UserTrustContext } from '@/contexts/user-trust-context'
+import { kinds } from 'nostr-tools'
import { type ReactNode, useCallback, useEffect, useState } from 'react'
import { useNostr } from './NostrProvider'
@@ -30,26 +32,30 @@ export function UserTrustProvider({ children }: { children: ReactNode }) {
setIsTrustLoaded(false)
const initWoT = async () => {
- const followListEvent = await replaceableEventService.fetchReplaceableEvent(currentPubkey, kinds.Contacts)
- const followings = followListEvent ? getPubkeysFromPTags(followListEvent.tags) : []
- followings.forEach((pubkey) => wotSet.add(pubkey.toLowerCase()))
+ try {
+ const followListEvent = await replaceableEventService.fetchReplaceableEvent(currentPubkey, kinds.Contacts)
+ const followings = followListEvent ? getPubkeysFromPTags(followListEvent.tags) : []
+ followings.forEach((pubkey) => wotSet.add(pubkey.toLowerCase()))
- const batchSize = 20
- for (let i = 0; i < followings.length; i += batchSize) {
- const batch = followings.slice(i, i + batchSize)
- await Promise.allSettled(
- batch.map(async (pubkey) => {
- const followListEvent = await replaceableEventService.fetchReplaceableEvent(pubkey, kinds.Contacts)
- const _followings = followListEvent ? getPubkeysFromPTags(followListEvent.tags) : []
- _followings.forEach((following) => {
- wotSet.add(following.toLowerCase())
+ const batchSize = 20
+ for (let i = 0; i < followings.length; i += batchSize) {
+ const batch = followings.slice(i, i + batchSize)
+ await Promise.allSettled(
+ batch.map(async (pubkey) => {
+ const innerFollow = await replaceableEventService.fetchReplaceableEvent(pubkey, kinds.Contacts)
+ const _followings = innerFollow ? getPubkeysFromPTags(innerFollow.tags) : []
+ _followings.forEach((following) => {
+ wotSet.add(following.toLowerCase())
+ })
})
- })
- )
- await new Promise((resolve) => setTimeout(resolve, 200))
+ )
+ await new Promise((resolve) => setTimeout(resolve, 200))
+ }
+ } finally {
+ setIsTrustLoaded(true)
}
}
- initWoT()
+ void initWoT()
}, [currentPubkey])
const isUserTrusted = useCallback(
diff --git a/src/services/client.service.ts b/src/services/client.service.ts
index 8f237673..8cfd0cec 100644
--- a/src/services/client.service.ts
+++ b/src/services/client.service.ts
@@ -3941,7 +3941,7 @@ class ClientService extends EventTarget {
const cacheKey = this.relayListRequestCacheKey(pubkey)
const existingRequest = this.relayListRequestCache.get(cacheKey)
if (existingRequest) {
- logger.debug('[FetchRelayList] Using cached in-flight request', { pubkey })
+ // Leader already logged `[FetchRelayList] Starting fetch`; joiners stay silent per burst.
return existingRequest
}
diff --git a/src/services/indexed-db.service.ts b/src/services/indexed-db.service.ts
index 9f7e2281..fd4efeac 100644
--- a/src/services/indexed-db.service.ts
+++ b/src/services/indexed-db.service.ts
@@ -270,6 +270,28 @@ class IndexedDbService {
private static readonly TOMBSTONE_NOT_CACHE_TTL_MS = 45_000
private static readonly TOMBSTONE_NOT_CACHE_MAX = 4096
+ /**
+ * During bulk hydrates, `getReplaceableEvent` can run hundreds of times in a short window.
+ * One sample slot per completed lookup; first few per window log in full, then sample.
+ */
+ private replaceableGetDebugWindow = { t0: 0, n: 0 }
+ private static readonly REPLACEABLE_GET_DEBUG_WINDOW_MS = 150
+ private static readonly REPLACEABLE_GET_DEBUG_BURST_AFTER = 10
+ private static readonly REPLACEABLE_GET_DEBUG_SAMPLE_EVERY = 24
+
+ private takeReplaceableGetDebugLogSlot(): boolean {
+ const now = Date.now()
+ const winMs = IndexedDbService.REPLACEABLE_GET_DEBUG_WINDOW_MS
+ if (now - this.replaceableGetDebugWindow.t0 > winMs) {
+ this.replaceableGetDebugWindow = { t0: now, n: 0 }
+ }
+ this.replaceableGetDebugWindow.n += 1
+ const n = this.replaceableGetDebugWindow.n
+ const burstAfter = IndexedDbService.REPLACEABLE_GET_DEBUG_BURST_AFTER
+ const sampleEvery = IndexedDbService.REPLACEABLE_GET_DEBUG_SAMPLE_EVERY
+ return n <= burstAfter || n % sampleEvery === 0
+ }
+
/** First TTL sweep after DB open (profile / relay list rows). */
private static readonly CLEANUP_INITIAL_DELAY_MS = 60 * 1000
/** Repeat TTL sweeps on this interval so pruning is not a one-shot. */
@@ -602,13 +624,16 @@ class IndexedDbService {
const request = store.get(key)
request.onsuccess = () => {
+ const allowDetailLog = this.takeReplaceableGetDebugLogSlot()
const row = request.result as TValue | undefined
if (!row) {
- logger.debug('[IndexedDB] getReplaceableEvent - no row found', {
- pubkey,
- kind,
- d
- })
+ if (allowDetailLog) {
+ logger.debug('[IndexedDB] getReplaceableEvent - no row found', {
+ pubkey,
+ kind,
+ d
+ })
+ }
transaction.commit()
return resolve(undefined)
}
@@ -619,19 +644,23 @@ class IndexedDbService {
if (isProfileOrPayment && row.addedAt && Date.now() - row.addedAt > PROFILE_AND_PAYMENT_CACHE_MAX_AGE_MS) {
// Profile is stale, but return it anyway - refresh will happen in background
// This prevents the "no profile" state when cache exists but is just old
- logger.debug('[IndexedDB] Profile cache is stale but returning anyway', {
+ if (allowDetailLog) {
+ logger.debug('[IndexedDB] Profile cache is stale but returning anyway', {
+ pubkey,
+ age: Date.now() - row.addedAt,
+ maxAge: PROFILE_AND_PAYMENT_CACHE_MAX_AGE_MS,
+ eventId: row.value?.id
+ })
+ }
+ }
+ if (allowDetailLog) {
+ logger.debug('[IndexedDB] getReplaceableEvent - found', {
pubkey,
- age: Date.now() - row.addedAt,
- maxAge: PROFILE_AND_PAYMENT_CACHE_MAX_AGE_MS,
- eventId: row.value?.id
+ kind,
+ eventId: row.value?.id,
+ addedAt: row.addedAt
})
}
- logger.debug('[IndexedDB] getReplaceableEvent - found', {
- pubkey,
- kind,
- eventId: row.value?.id,
- addedAt: row.addedAt
- })
transaction.commit()
resolve(row.value)
}