Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
79c7e8c0cc
  1. 1
      src/components/Note/index.tsx
  2. 88
      src/components/SearchResult/FullTextSearchByRelay.tsx
  3. 34
      src/lib/relay-list-builder.ts
  4. 36
      src/services/client-events.service.ts
  5. 83
      src/services/client-query.service.ts
  6. 230
      src/services/client-replaceable-events.service.ts
  7. 25
      src/services/client.service.ts
  8. 47
      src/services/indexed-db.service.ts
  9. 87
      src/services/note-stats.service.ts

1
src/components/Note/index.tsx

@ -392,7 +392,6 @@ export default function Note({ @@ -392,7 +392,6 @@ export default function Note({
let content: React.ReactNode
if (!isRenderableNoteKind(event.kind)) {
logger.debug('Note component - rendering UnknownNote for unsupported kind:', event.kind)
content = <UnknownNote className="mt-2" event={displayEvent} omitKindLabel />
} else if (muteSetHas(mutePubkeySet, event.pubkey) && !showMuted) {
content = <MutedNote show={() => setShowMuted(true)} />

88
src/components/SearchResult/FullTextSearchByRelay.tsx

@ -2,7 +2,6 @@ import NoteCard from '@/components/NoteCard' @@ -2,7 +2,6 @@ import NoteCard from '@/components/NoteCard'
import RelayIcon from '@/components/RelayIcon'
import { Skeleton } from '@/components/ui/skeleton'
import { compareEventsForDTagQuery } from '@/lib/dtag-search'
import logger from '@/lib/logger'
import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey'
import { normalizeUrl } from '@/lib/url'
import { NoteFeedProfileContext, type NoteFeedProfileContextValue } from '@/providers/NoteFeedProfileContext'
@ -213,24 +212,6 @@ function sortRelaysByHost(urls: readonly string[]): string[] { @@ -213,24 +212,6 @@ function sortRelaysByHost(urls: readonly string[]): string[] {
)
}
/** Console hint: what this one-shot outcome suggests about NIP-50 (never proof without NIP-11). */
function nip50OutcomeHint(args: {
phase: 'done' | 'error'
rawCount: number
connectionError?: string
}): string {
if (args.phase === 'error') {
return 'no_transport_or_relay_closed_request — cannot tell NIP-50 from this run'
}
if (args.rawCount > 0) {
return 'returned_events_for_REQ_with_search_field — relay likely honors NIP-50 for this query (verify with NIP-11 supported_nips)'
}
if (args.connectionError) {
return 'zero_events_but_connection_error_message — partial failure or restrictive CLOSE; NIP-50 unclear'
}
return 'zero_events_clean_close — no_hits_or_search_ignored_or_empty_index — cannot distinguish without NIP-11 or a known match'
}
export default function FullTextSearchByRelay({
searchQuery,
relayUrls,
@ -325,15 +306,6 @@ export default function FullTextSearchByRelay({ @@ -325,15 +306,6 @@ export default function FullTextSearchByRelay({
}
const runOneRelay = async (relayUrl: string) => {
const host = relayHostForSubscribeLog(relayUrl)
logger.debug('[NIP-50 full-text] card_begin', {
runId: myRun,
relayUrl,
host,
timeoutMs: FULL_TEXT_SEARCH_PER_RELAY_TIMEOUT_MS,
filter: { search: filter.search, kinds: filter.kinds, limit: filter.limit }
})
const t0 = performance.now()
try {
const { events: raw, connectionError } = await client.fetchEventsFromSingleRelay(
@ -350,18 +322,6 @@ export default function FullTextSearchByRelay({ @@ -350,18 +322,6 @@ export default function FullTextSearchByRelay({
if (myRun !== runGeneration.current) return
const ms = Math.round(performance.now() - t0)
if (sorted.length === 0 && connectionError) {
logger.debug('[NIP-50 full-text] card_end', {
runId: myRun,
relayUrl,
host,
phase: 'error' as const,
ms,
eventCountRaw: raw.length,
eventCountShown: 0,
connectionError,
cardErrorMessage: connectionError,
nip50Hint: nip50OutcomeHint({ phase: 'error', rawCount: 0, connectionError })
})
setRelayRows((prev) =>
prev.map((r) =>
r.relayUrl === relayUrl
@ -374,28 +334,6 @@ export default function FullTextSearchByRelay({ @@ -374,28 +334,6 @@ export default function FullTextSearchByRelay({
mergeIntoHits(relayUrl, sorted)
logger.debug('[NIP-50 full-text] card_end', {
runId: myRun,
relayUrl,
host,
phase: 'done' as const,
ms,
eventCountRaw: raw.length,
eventCountShown: sorted.length,
connectionError: sorted.length > 0 ? undefined : connectionError,
cardNote:
sorted.length === 0 && connectionError
? 'UI shows soft warning (empty with message)'
: sorted.length === 0
? 'UI empty state'
: 'UI lists notes',
nip50Hint: nip50OutcomeHint({
phase: 'done',
rawCount: raw.length,
connectionError: sorted.length > 0 ? undefined : connectionError
})
})
setRelayRows((prev) =>
prev.map((r) =>
r.relayUrl === relayUrl
@ -413,18 +351,6 @@ export default function FullTextSearchByRelay({ @@ -413,18 +351,6 @@ export default function FullTextSearchByRelay({
if (myRun !== runGeneration.current) return
const msg = err instanceof Error ? err.message : String(err)
const ms = Math.round(performance.now() - t0)
logger.debug('[NIP-50 full-text] card_end', {
runId: myRun,
relayUrl,
host,
phase: 'error' as const,
ms,
eventCountRaw: 0,
eventCountShown: 0,
connectionError: undefined,
cardErrorMessage: msg,
nip50Hint: nip50OutcomeHint({ phase: 'error', rawCount: 0 })
})
setRelayRows((prev) =>
prev.map((r) =>
r.relayUrl === relayUrl ? { ...r, phase: 'error', eventCount: 0, ms, errorMessage: msg } : r
@ -442,25 +368,11 @@ export default function FullTextSearchByRelay({ @@ -442,25 +368,11 @@ export default function FullTextSearchByRelay({
}
void (async () => {
logger.debug('[NIP-50 full-text] wave_begin', {
runId: myRun,
query: q,
relayCount: normalizedRelays.length,
concurrency: poolSize,
filter: { search: filter.search, kinds: filter.kinds, limit: filter.limit },
relays: normalizedRelays.map((u) => ({ url: u, host: relayHostForSubscribeLog(u) }))
})
try {
await Promise.all(Array.from({ length: poolSize }, () => worker()))
} catch {
/* runOneRelay already updates relay rows */
}
if (myRun !== runGeneration.current) return
logger.debug('[NIP-50 full-text] wave_end', {
runId: myRun,
relayCount: normalizedRelays.length,
note: 'matches UI "all relays finished" when every relay row is done or error'
})
})()
return cleanupInvalidatePreviousRun

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

@ -150,13 +150,8 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio @@ -150,13 +150,8 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio
authorOutboxes.forEach(addRelay)
const authorInboxes = userReadRelaysWithHttp(authorRelayList).slice(0, 10)
authorInboxes.forEach(addRelay)
logger.debug('[RelayListBuilder] Added author relays', {
author: authorPubkey.substring(0, 8),
outboxes: authorOutboxes.length,
inboxes: authorInboxes.length
})
} catch (error) {
logger.debug('[RelayListBuilder] Failed to read author relay list from storage', { error })
logger.warn('[RelayListBuilder] Failed to read author relay list from storage', { error })
}
}
@ -176,28 +171,16 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio @@ -176,28 +171,16 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio
}
// 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.warn('[RelayListBuilder] Failed to fetch user favorite relays', { error })
}
}
logger.debug('[RelayListBuilder] Added user own relays', {
read: (userRelayList.read || []).length,
write: (userRelayList.write || []).length,
local: includeLocalRelays ? (await getCacheRelayUrls(userPubkey)).length : 0,
favorite: favoriteRelaysCount
})
} catch (error) {
logger.debug('[RelayListBuilder] Failed to fetch user relay list', { error })
logger.warn('[RelayListBuilder] Failed to fetch user relay list', { error })
}
} else if (userPubkey) {
// Even if not including user's own relays, still include user's inboxes for reading
@ -217,15 +200,12 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio @@ -217,15 +200,12 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio
try {
const favoriteRelays = await client.fetchFavoriteRelays(userPubkey)
favoriteRelays.forEach(addRelay)
logger.debug('[RelayListBuilder] Added user favorite relays (with inboxes path)', {
count: favoriteRelays.length
})
} catch (error) {
logger.debug('[RelayListBuilder] Failed to fetch user favorite relays', { error })
logger.warn('[RelayListBuilder] Failed to fetch user favorite relays', { error })
}
}
} catch (error) {
logger.debug('[RelayListBuilder] Failed to fetch user inboxes', { error })
logger.warn('[RelayListBuilder] Failed to fetch user inboxes', { error })
}
}
@ -362,7 +342,7 @@ export async function buildPollResultsReadRelayUrls(options: { @@ -362,7 +342,7 @@ export async function buildPollResultsReadRelayUrls(options: {
viewerReadSlice = userReadRelaysWithHttp(viewerRl).slice(0, POLL_RESULTS_NIP65_READ_SLICE)
}
} catch {
logger.debug('[RelayListBuilder] poll results: NIP-65 relay list race failed')
/* ignore — poll results still use other layers */
}
pushLayer(viewerReadSlice)
@ -373,7 +353,7 @@ export async function buildPollResultsReadRelayUrls(options: { @@ -373,7 +353,7 @@ export async function buildPollResultsReadRelayUrls(options: {
const localRelays = await getCacheRelayUrls(viewerPubkey)
pushLayer(localRelays)
} catch {
logger.debug('[RelayListBuilder] poll results: cache relays failed')
/* ignore */
}
}

36
src/services/client-events.service.ts

@ -503,34 +503,12 @@ export class EventService { @@ -503,34 +503,12 @@ export class EventService {
filter.ids?.length === 1 && /^[0-9a-f]{64}$/i.test(String(filter.ids[0]))
? { explicitNoteLookupHexId: String(filter.ids[0]).toLowerCase() }
: undefined
const logKey =
'ids' in filter && filter.ids?.[0]
? filter.ids[0].slice(0, 8)
: Array.isArray(filter['#a']) && filter['#a'][0]
? String(filter['#a'][0]).slice(0, 40)
: `${filter.kinds?.[0]}:${(filter.authors?.[0] ?? '').slice(0, 8)}`
logger.debug('fetchEventWithExternalRelays: Starting search', {
noteIdKey: logKey,
relayCount: externalRelays.length,
relays: externalRelays
})
const startTime = Date.now()
/** User-driven “try everywhere”: wait for EOSE-ish completion so slower relays (e.g. nos.lol) can answer. */
const events = await this.queryService.query(externalRelays, filter, undefined, {
eoseTimeout: 12_000,
globalTimeout: 35_000,
immediateReturn: false
})
const duration = Date.now() - startTime
logger.debug('fetchEventWithExternalRelays: Search completed', {
noteIdKey: logKey,
relayCount: externalRelays.length,
eventsFound: events.length,
durationMs: duration
})
const usable = events
.filter((e) => !shouldDropEventOnIngest(e, ingestOpts))
@ -1249,13 +1227,6 @@ export class EventService { @@ -1249,13 +1227,6 @@ export class EventService {
return undefined
}
logger.debug('[EventService] Using comprehensive relay list', {
author: authorPubkey?.substring(0, 8),
relayCount: relayUrls.length,
hasHints: relayHints.length > 0,
hasSeen: seenRelays.length > 0
})
const isSingleEventById = Boolean(filter.ids && filter.ids.length === 1 && filter.limit === 1)
/** Replaceable coordinate: `#a` (preferred) or legacy `authors` + `#d`. */
const isReplaceableCoordinateFetch =
@ -1275,13 +1246,6 @@ export class EventService { @@ -1275,13 +1246,6 @@ export class EventService {
.filter((e) => !shouldDropEventOnIngest(e, ingestOpts))
.sort((a, b) => b.created_at - a.created_at)[0]
if (event && isSingleEventById && !isReplaceableEvent(event.kind)) {
logger.debug('[EventService] Non-replaceable event returned immediately', {
eventId: event.id.substring(0, 8),
kind: event.kind
})
}
return event
}

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

@ -29,13 +29,7 @@ import { applyRelayNip42AckTimeout } from '@/lib/relay-nip42-tuning' @@ -29,13 +29,7 @@ import { applyRelayNip42AckTimeout } from '@/lib/relay-nip42-tuning'
import { isIndexRelayTransportFailure, queryIndexRelay } from '@/lib/index-relay-http'
import logger from '@/lib/logger'
import { isHttpRelayUrl, normalizeHttpRelayUrl, normalizeUrl } from '@/lib/url'
import {
RelaySubscribeOpBatch,
compactFilterForRelayLog,
humanizeSubscribeTerminalDetail,
relayHostForSubscribeLog,
type RelayOpTerminalRow
} from '@/services/relay-operation-log.service'
import { RelaySubscribeOpBatch, type RelayOpTerminalRow } from '@/services/relay-operation-log.service'
import { patchRelayNoticeForFetchFailures } from '@/services/relay-notice-fetch-failure'
import type { Filter, Event as NEvent } from 'nostr-tools'
import { SimplePool, EventTemplate, VerifiedEvent, nip19 } from 'nostr-tools'
@ -82,54 +76,44 @@ function logQueryReqConsolidatedEnd( @@ -82,54 +76,44 @@ function logQueryReqConsolidatedEnd(
kindHistogram[k] = (kindHistogram[k] ?? 0) + 1
}
const norm = (u: string) => normalizeUrl(u) || u
type Row = {
url: string
host: string
terminal?: RelayOpTerminalRow['outcome']
detail?: string
eventsReturned: number
}
const byKey = new Map<string, Row>()
const rowFor = (url: string): Row => {
const key = norm(url)
let r = byKey.get(key)
if (!r) {
r = { url: key, host: relayHostForSubscribeLog(key), eventsReturned: 0 }
byKey.set(key, r)
}
return r
}
const outcomeCounts: Record<string, number> = {}
for (const t of terminals) {
const r = rowFor(t.relayUrl)
r.terminal = t.outcome
r.detail = humanizeSubscribeTerminalDetail(t.outcome, t.detail)
const k = t.outcome
outcomeCounts[k] = (outcomeCounts[k] ?? 0) + 1
}
for (const e of events) {
const seen = getSeenForEvent(e.id)
for (const u of seen) {
rowFor(u).eventsReturned += 1
}
const benignEmpty =
events.length === 0 &&
(terminals.length === 0 ||
terminals.every((t) => t.outcome === 'eose'))
if (benignEmpty) {
return
}
for (const u of inputRelays) {
rowFor(u)
}
for (const b of httpBases) {
rowFor(b)
const relayTotal = new Set([
...inputRelays.map((u) => normalizeUrl(u) || u),
...httpBases.map((u) => normalizeUrl(u) || u)
]).size
let relaysWithHits = 0
if (events.length > 0) {
const hitUrls = new Set<string>()
for (const e of events) {
for (const u of getSeenForEvent(e.id)) {
hitUrls.add(normalizeUrl(u) || u)
}
}
relaysWithHits = hitUrls.size
}
const perRelay = [...byKey.values()].sort((a, b) => a.host.localeCompare(b.host))
logger.debug('[QueryService] req_end', {
reqId,
source,
eventCount: events.length,
kindHistogram,
perRelay
relayCandidateCount: relayTotal,
terminalCount: terminals.length,
terminalOutcomes: outcomeCounts,
relaysWithEventHits: relaysWithHits
})
}
@ -436,17 +420,6 @@ export class QueryService { @@ -436,17 +420,6 @@ export class QueryService {
const inputRelaysOrdered = Array.from(new Set(urls.map((u) => normalizeUrl(u) || u).filter(Boolean)))
const wsRelayCandidates = Array.from(new Set(wsQueryUrls.map((u) => normalizeUrl(u) || u).filter(Boolean)))
logger.debug('[QueryService] req_begin', {
reqId,
source,
relays: inputRelaysOrdered,
httpRelayBases,
wsRelayCandidates,
filters: sanitizedFilters.map(compactFilterForRelayLog),
eoseTimeout,
globalTimeout
})
return await new Promise<NEvent[]>((resolve) => {
const events: NEvent[] = []
const abortHttp = new AbortController()

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

@ -186,14 +186,7 @@ export class ReplaceableEventService { @@ -186,14 +186,7 @@ export class ReplaceableEventService {
containingEventRelays: string[] = []
): Promise<NEvent | undefined> {
const cacheKey = d ? `${kind}:${pubkey}:${d}` : `${kind}:${pubkey}`
logger.debug('[ReplaceableEventService] fetchReplaceableEvent start', {
pubkey,
kind,
d,
cacheKey,
containingEventRelays: containingEventRelays.length
})
try {
if (kind === kinds.Metadata && !d) {
const sessionEv = client.eventService.getSessionMetadataForPubkey(pubkey)
@ -212,9 +205,6 @@ export class ReplaceableEventService { @@ -212,9 +205,6 @@ export class ReplaceableEventService {
containingEventRelays.length === 0 &&
ReplaceableEventService.isProfileFetchMissCached(pubkey)
) {
logger.debug('[ReplaceableEventService] Skipping metadata fetch (recent profile miss cache)', {
pubkey
})
return undefined
}
@ -247,18 +237,9 @@ export class ReplaceableEventService { @@ -247,18 +237,9 @@ export class ReplaceableEventService {
let event: NEvent | undefined
if (containingEventRelays.length > 0 && kind === kinds.Metadata && !d) {
// For profiles with containing event relays (author's relay list), check IndexedDB first, then query directly
logger.debug('[ReplaceableEventService] Checking IndexedDB for profile with containing relays', {
pubkey,
kind
})
try {
const indexedDbCached = await indexedDb.getReplaceableEvent(pubkey, kind, d)
if (indexedDbCached) {
logger.debug('[ReplaceableEventService] Found in IndexedDB', {
pubkey,
kind,
eventId: indexedDbCached.id
})
// Refresh in background
this.refreshInBackground(pubkey, kind, d).catch(() => {})
return indexedDbCached
@ -270,19 +251,9 @@ export class ReplaceableEventService { @@ -270,19 +251,9 @@ export class ReplaceableEventService {
error: error instanceof Error ? error.message : String(error)
})
}
// Not in IndexedDB, fetch from network with custom relay list
logger.debug('[ReplaceableEventService] Building relay list with containing event relays', {
pubkey,
containingRelayCount: containingEventRelays.length
})
const relayUrls = await this.buildComprehensiveRelayListForAuthor(pubkey, kind, containingEventRelays, [])
logger.debug('[ReplaceableEventService] Querying relays', {
pubkey,
relayCount: relayUrls.length,
relays: relayUrls.slice(0, 5)
})
const startTime = Date.now()
const events = await this.queryService.query(
relayUrls,
{
@ -296,51 +267,19 @@ export class ReplaceableEventService { @@ -296,51 +267,19 @@ export class ReplaceableEventService {
globalTimeout: METADATA_BATCH_QUERY_GLOBAL_TIMEOUT_MS
}
)
const queryTime = Date.now() - startTime
logger.debug('[ReplaceableEventService] Query completed', {
pubkey,
eventCount: events.length,
queryTime: `${queryTime}ms`
})
const sortedEvents = events.sort((a, b) => b.created_at - a.created_at)
event = sortedEvents.length > 0 ? sortedEvents[0] : undefined
} else {
// Use DataLoader for batching (IndexedDB checks and network fetches are batched)
logger.debug('[ReplaceableEventService] Using DataLoader (batches IndexedDB + network)', {
pubkey,
kind,
d
})
const startTime = Date.now()
const loadedEvent = d
? await this.replaceableEventDataLoader.load({ pubkey, kind, d })
: await this.replaceableEventFromBigRelaysDataloader.load({ pubkey, kind })
const loadTime = Date.now() - startTime
logger.debug('[ReplaceableEventService] DataLoader completed', {
pubkey,
found: !!loadedEvent,
loadTime: `${loadTime}ms`
})
event = loadedEvent || undefined
}
if (event) {
logger.debug('[ReplaceableEventService] Event found', {
pubkey,
kind,
eventId: event.id,
created_at: event.created_at
})
return event
}
// Log when no event is found (helps debug relay failures)
if (kind === kinds.Metadata) {
logger.debug('[ReplaceableEventService] No profile found for pubkey', {
pubkey,
cacheKey
})
}
} catch (error) {
// Log errors but don't throw - return undefined so UI can show fallback
if (kind === kinds.Metadata) {
@ -357,11 +296,7 @@ export class ReplaceableEventService { @@ -357,11 +296,7 @@ export class ReplaceableEventService {
})
}
}
logger.debug('[ReplaceableEventService] fetchReplaceableEvent returning undefined', {
pubkey,
kind
})
return undefined
}
@ -486,19 +421,6 @@ export class ReplaceableEventService { @@ -486,19 +421,6 @@ export class ReplaceableEventService {
private async replaceableEventFromBigRelaysBatchLoadFn(
params: readonly { pubkey: string; kind: number }[]
): Promise<(NEvent | null)[]> {
// CRITICAL: Reduce logging during rapid scrolling - only log large batches
if (params.length > 50) {
logger.debug('[ReplaceableEventService] Large batch load function called', {
paramCount: params.length,
kind: params[0]?.kind
})
} else {
logger.debug('[ReplaceableEventService] Batch load function called', {
paramCount: params.length,
kind: params[0]?.kind
})
}
const results: (NEvent | null)[] = new Array(params.length).fill(null)
const eventsMap = new Map<string, NEvent>()
@ -532,11 +454,6 @@ export class ReplaceableEventService { @@ -532,11 +454,6 @@ export class ReplaceableEventService {
const pubkeys = items.map((x) => x.pubkey)
try {
const indexedDbEvents = await indexedDb.getManyReplaceableEvents(pubkeys, kind)
logger.debug('[ReplaceableEventService] IndexedDB batch query completed', {
kind,
pubkeyCount: pubkeys.length,
foundCount: indexedDbEvents.filter((e) => e !== null && e !== undefined).length
})
items.forEach(({ pubkey, index }, idx) => {
const event = indexedDbEvents[idx]
@ -581,9 +498,6 @@ export class ReplaceableEventService { @@ -581,9 +498,6 @@ export class ReplaceableEventService {
// Step 2: Only fetch missing events from network
if (missingParams.length === 0) {
logger.debug('[ReplaceableEventService] All events resolved (session + IndexedDB), skipping network fetch', {
totalCount: params.length
})
return results
}
@ -604,19 +518,6 @@ export class ReplaceableEventService { @@ -604,19 +518,6 @@ export class ReplaceableEventService {
}
if (networkMissing.length > 0) {
// Only log at info level for large batches
if (networkMissing.length > 50) {
logger.debug('[ReplaceableEventService] Fetching missing events from network', {
missingCount: networkMissing.length,
totalCount: params.length
})
} else {
logger.debug('[ReplaceableEventService] Fetching missing events from network', {
missingCount: networkMissing.length,
totalCount: params.length
})
}
// Group missing params by kind for network fetch
const missingGroups = new Map<number, { pubkey: string; index: number }[]>()
networkMissing.forEach(({ pubkey, kind, index }) => {
@ -714,21 +615,6 @@ export class ReplaceableEventService { @@ -714,21 +615,6 @@ export class ReplaceableEventService {
} else {
relayUrls = [...FAST_READ_RELAY_URLS]
}
// Only log at info level for large batches
if (pubkeys.length > 50) {
logger.debug('[ReplaceableEventService] Starting query for large batch', {
kind,
pubkeyCount: pubkeys.length,
relayCount: relayUrls.length
})
} else {
logger.debug('[ReplaceableEventService] Starting query for batch', {
kind,
pubkeyCount: pubkeys.length,
relayCount: relayUrls.length
})
}
// Contacts + NIP-65 need the same patience as pins/payment: 100ms EOSE loses the race on slow relays
// and multi-author batches must not use replaceableRace (first EVENT may not be the latest per author).
const isSlowReplaceableBatch =
@ -778,21 +664,7 @@ export class ReplaceableEventService { @@ -778,21 +664,7 @@ export class ReplaceableEventService {
queryOpts
)
}
// Only log at info level for large batches or if many events found
if (pubkeys.length > 50 || events.length > 100) {
logger.debug('[ReplaceableEventService] Query completed for batch', {
kind,
pubkeyCount: pubkeys.length,
eventCount: events.length
})
} else {
logger.debug('[ReplaceableEventService] Query completed for batch', {
kind,
pubkeyCount: pubkeys.length,
eventCount: events.length
})
}
// CRITICAL: Limit the number of events processed to prevent memory issues during rapid scrolling
// If we have too many events, only process the most recent ones per pubkey
if (events.length > 1000) {
@ -812,10 +684,6 @@ export class ReplaceableEventService { @@ -812,10 +684,6 @@ export class ReplaceableEventService {
}
// Convert back to array, but limit to reasonable size
const limitedEvents = Array.from(eventsByPubkey.values()).slice(0, 500)
logger.debug('[ReplaceableEventService] Limited batch size', {
originalCount: events.length,
limitedCount: limitedEvents.length
})
// Use limited events for processing
for (const event of limitedEvents) {
const key = `${event.pubkey}:${event.kind}`
@ -863,22 +731,9 @@ export class ReplaceableEventService { @@ -863,22 +731,9 @@ export class ReplaceableEventService {
/* ignore */
}
}
// Log when no events are found (helps debug relay failures)
if (kind === kinds.Metadata && events.length === 0 && pubkeys.length > 0) {
logger.debug('[ReplaceableEventService] No profile events found from relays', {
pubkeyCount: pubkeys.length,
relayCount: relayUrls.length,
relays: relayUrls.slice(0, 3) // Show first 3 for brevity
})
}
})
)
} else {
logger.debug('[ReplaceableEventService] All missing events resolved from session, skipping network fetch', {
totalCount: params.length
})
}
// Step 3: Persist hits only. Do not write negative cache rows (`value: null`) — optional kinds
@ -892,21 +747,7 @@ export class ReplaceableEventService { @@ -892,21 +747,7 @@ export class ReplaceableEventService {
}
})
)
// Only log at info level for large batches
if (params.length > 50) {
logger.debug('[ReplaceableEventService] Batch load function completed', {
paramCount: params.length,
foundCount: results.filter(r => r !== null).length,
indexedDbCount: params.length - missingParams.length,
networkCount: missingParams.length
})
} else {
logger.debug('[ReplaceableEventService] Batch load function completed', {
paramCount: params.length,
foundCount: results.filter(r => r !== null).length
})
}
return results
}
@ -1000,17 +841,13 @@ export class ReplaceableEventService { @@ -1000,17 +841,13 @@ export class ReplaceableEventService {
* Fetch profile event by id (hex, npub, nprofile)
*/
async fetchProfileEvent(id: string, _skipCache: boolean = false): Promise<NEvent | undefined> {
logger.debug('[ReplaceableEventService] fetchProfileEvent start', { id })
let pubkey: string | undefined
let relays: string[] = []
if (/^[0-9a-f]{64}$/.test(id)) {
pubkey = id
logger.debug('[ReplaceableEventService] ID is hex pubkey', { pubkey })
} else {
try {
const { data, type } = nip19.decode(id)
logger.debug('[ReplaceableEventService] Decoded bech32 ID', { type })
switch (type) {
case 'npub':
pubkey = data
@ -1018,7 +855,6 @@ export class ReplaceableEventService { @@ -1018,7 +855,6 @@ export class ReplaceableEventService {
case 'nprofile':
pubkey = data.pubkey
if (data.relays) relays = data.relays
logger.debug('[ReplaceableEventService] nprofile has relay hints', { relayCount: relays.length })
break
}
} catch (error) {
@ -1062,21 +898,11 @@ export class ReplaceableEventService { @@ -1062,21 +898,11 @@ export class ReplaceableEventService {
// CRITICAL: Do NOT pass relay hints here - passing any relays bypasses DataLoader and creates individual subscriptions
// DataLoader already uses default relays internally and batches all profile fetches
// We'll use relay hints in Step 2/3 only if Step 1 fails
logger.debug('[ReplaceableEventService] Step 1: Trying with DataLoader (checks cache first, uses default relays, batched)', {
pubkey,
relayHintCount: relayHints.length,
hasRelayHints: relayHints.length > 0
})
// fetchReplaceableEvent uses DataLoader which checks IndexedDB first, then queries default relays
// Passing empty array ensures DataLoader is used (batched) - this prevents individual subscriptions
const profileEvent = await this.fetchReplaceableEvent(pubkey, kinds.Metadata, undefined, [])
if (profileEvent) {
logger.debug('[ReplaceableEventService] Profile found via cache / default relays (DataLoader)', {
pubkey,
eventId: profileEvent.id
})
await this.indexProfile(profileEvent)
return profileEvent
}
@ -1084,24 +910,15 @@ export class ReplaceableEventService { @@ -1084,24 +910,15 @@ export class ReplaceableEventService {
await ReplaceableEventService.acquireProfileFallbackNetworkSlot()
try {
// Step 2: Only after cache + default relays miss — NIP-65 relay list (timeout-capped), then hints + outbox/inbox + defaults.
logger.debug('[ReplaceableEventService] Step 2: Fetching author relay list as fallback', {
pubkey,
relayHintCount: relayHints.length
})
let authorRelayList: { read?: string[]; write?: string[] } | null = null
try {
const hasLocal10002 = await ReplaceableEventService.hasRelayListInLocalCache(pubkey)
if (hasLocal10002) {
authorRelayList = await client.peekRelayListFromStorage(pubkey)
logger.debug('[ReplaceableEventService] Step 2: using cached kind 10002 (skip fetchRelayList network)', {
pubkey
})
} else {
const relayListPromise = client.fetchRelayList(pubkey)
const timeoutPromise = new Promise<null>((resolve) => {
setTimeout(() => {
logger.debug('[ReplaceableEventService] fetchRelayList timeout, giving up', { pubkey })
resolve(null)
}, 2800)
})
@ -1140,16 +957,11 @@ export class ReplaceableEventService { @@ -1140,16 +957,11 @@ export class ReplaceableEventService {
expandedRelays
)
if (profileFromExpanded) {
logger.debug('[ReplaceableEventService] Profile found after relay-list fallback', {
pubkey,
eventId: profileFromExpanded.id
})
await this.indexProfile(profileFromExpanded)
return profileFromExpanded
}
// Step 3: Last resort — broad relay query (timeout-bounded in query layer)
logger.debug('[ReplaceableEventService] Step 3: Comprehensive relay query (last resort)', { pubkey })
try {
const userPubkey = client.pubkey
const comprehensiveRelays = await buildComprehensiveRelayList({
@ -1165,14 +977,7 @@ export class ReplaceableEventService { @@ -1165,14 +977,7 @@ export class ReplaceableEventService {
includeLocalRelays: true
})
logger.debug('[ReplaceableEventService] Comprehensive relay list built', {
pubkey,
relayCount: comprehensiveRelays.length,
relays: comprehensiveRelays.slice(0, 10)
})
if (comprehensiveRelays.length > 0) {
const startTime = Date.now()
const events = await this.queryService.query(
comprehensiveRelays,
{
@ -1186,22 +991,10 @@ export class ReplaceableEventService { @@ -1186,22 +991,10 @@ export class ReplaceableEventService {
globalTimeout: 3500
}
)
const queryTime = Date.now() - startTime
logger.debug('[ReplaceableEventService] Comprehensive search completed', {
pubkey,
eventCount: events.length,
queryTime: `${queryTime}ms`,
relayCount: comprehensiveRelays.length
})
if (events.length > 0) {
const sortedEvents = events.sort((a, b) => b.created_at - a.created_at)
const found = sortedEvents[0]!
logger.debug('[ReplaceableEventService] Profile found via comprehensive search', {
pubkey,
eventId: found.id
})
await this.indexProfile(found)
return found
}
@ -1216,10 +1009,6 @@ export class ReplaceableEventService { @@ -1216,10 +1009,6 @@ export class ReplaceableEventService {
ReplaceableEventService.releaseProfileFallbackNetworkSlot()
}
logger.debug('[ReplaceableEventService] Profile not found after cache, relay-list fallback, and comprehensive search', {
pubkey,
triedRelayHints: relayHints.length > 0
})
if (!_skipCache && relayHints.length === 0) {
ReplaceableEventService.rememberProfileFetchMiss(pubkey)
}
@ -1676,15 +1465,6 @@ export class ReplaceableEventService { @@ -1676,15 +1465,6 @@ export class ReplaceableEventService {
result.push([relayUrl, Array.from(pubkeys)])
}
logger.debug('[ReplaceableEventService] fetchFollowingFavoriteRelays completed', {
followingsCount: followings.length,
processedCount: followingsToProcess.length,
favoriteRelaysEventsFound: favoriteRelaysEvents.filter((e) => e !== undefined).length,
relayListEventsFound: relayListEvents.filter((e) => e !== undefined).length,
uniqueRelays: result.length,
totalUsers: result.reduce((sum, [, users]) => sum + users.length, 0)
})
return result
}
}

25
src/services/client.service.ts

@ -3941,23 +3941,12 @@ class ClientService extends EventTarget { @@ -3941,23 +3941,12 @@ class ClientService extends EventTarget {
const cacheKey = this.relayListRequestCacheKey(pubkey)
const existingRequest = this.relayListRequestCache.get(cacheKey)
if (existingRequest) {
// Leader already logged `[FetchRelayList] Starting fetch`; joiners stay silent per burst.
return existingRequest
}
logger.debug('[FetchRelayList] Starting fetch', { pubkey })
const requestPromise = (async () => {
try {
const startTime = Date.now()
const [relayList] = await this.fetchRelayLists([pubkey])
const duration = Date.now() - startTime
logger.debug('[FetchRelayList] Fetch completed', {
pubkey,
duration: `${duration}ms`,
hasRelayList: !!relayList,
writeCount: relayList?.write?.length ?? 0,
readCount: relayList?.read?.length ?? 0
})
return relayList
} catch (error) {
logger.warn('[FetchRelayList] Fetch failed; using IndexedDB / defaults', {
@ -4213,14 +4202,6 @@ class ClientService extends EventTarget { @@ -4213,14 +4202,6 @@ class ClientService extends EventTarget {
const missing10002Pubkeys = pubkeys.filter((_pk, i) => storedRelayEvents[i] == null)
if (missing10002Pubkeys.length > 0) {
logger.debug(
'[FetchRelayLists] Kind 10002 missing in IndexedDB for some pubkeys; fetching only those over the network',
{
batchSize: pubkeys.length,
missingCount: missing10002Pubkeys.length,
missingPubkeyPrefixes: missing10002Pubkeys.map((p) => p.slice(0, 12))
}
)
const [relFetched, httpFetched] = await Promise.all([
this.replaceableEventService.fetchReplaceableEventsFromProfileFetchRelays(
missing10002Pubkeys,
@ -4269,10 +4250,6 @@ class ClientService extends EventTarget { @@ -4269,10 +4250,6 @@ class ClientService extends EventTarget {
if (allHaveKind10002) {
this.refreshRelayListsFromNetwork(pubkeys, storedRelayEvents)
logger.debug(
'[FetchRelayLists] Kind 10002 present in IndexedDB for all pubkeys; merging locally, network refresh in background',
{ count: pubkeys.length }
)
const cacheRelayEvents = await Promise.race([
this.fetchCacheRelayEventsFromMultipleSources(pubkeys, storedRelayEvents, storedRelayEvents),
new Promise<(NEvent | null | undefined)[]>((resolve) =>

47
src/services/indexed-db.service.ts

@ -270,29 +270,6 @@ class IndexedDbService { @@ -270,29 +270,6 @@ 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. */
private static readonly CLEANUP_INTERVAL_MS = 60 * 60 * 1000
@ -624,16 +601,8 @@ class IndexedDbService { @@ -624,16 +601,8 @@ class IndexedDbService {
const request = store.get(key)
request.onsuccess = () => {
const allowDetailLog = this.takeReplaceableGetDebugLogSlot()
const row = request.result as TValue<Event> | undefined
if (!row) {
if (allowDetailLog) {
logger.debug('[IndexedDB] getReplaceableEvent - no row found', {
pubkey,
kind,
d
})
}
transaction.commit()
return resolve(undefined)
}
@ -644,22 +613,6 @@ class IndexedDbService { @@ -644,22 +613,6 @@ 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
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,
kind,
eventId: row.value?.id,
addedAt: row.addedAt
})
}
transaction.commit()
resolve(row.value)

87
src/services/note-stats.service.ts

@ -137,10 +137,6 @@ class NoteStatsService { @@ -137,10 +137,6 @@ class NoteStatsService {
}, this.BATCH_DELAY)
}
private statsPendingSize() {
return this.pendingForeground.size + this.pendingEvents.size
}
/** Up to {@link MAX_BATCH_SIZE} ids, foreground queue first (same insertion order within each set). */
private takeNextStatsSlice(): string[] {
const out: string[] = []
@ -186,7 +182,6 @@ class NoteStatsService { @@ -186,7 +182,6 @@ class NoteStatsService {
opts?: { foreground?: boolean }
) {
const eventId = this.statsKey(event.id)
const idShort = `${eventId.slice(0, 12)}`
const foreground = opts?.foreground === true
const rememberRoot = () => {
@ -204,12 +199,6 @@ class NoteStatsService { @@ -204,12 +199,6 @@ class NoteStatsService {
this.pendingEvents.delete(eventId)
this.pendingForeground.add(eventId)
}
logger.debug('[NoteStats] fetchNoteStats: merged into existing pending batch', {
eventId: idShort,
kind: event.kind,
pendingForeground: this.pendingForeground.size,
pendingBackground: this.pendingEvents.size
})
this.maybeFlushStatsBatch(foreground)
return
}
@ -220,10 +209,6 @@ class NoteStatsService { @@ -220,10 +209,6 @@ class NoteStatsService {
if (foreground) {
this.deferredRequeueForeground.add(eventId)
}
logger.debug('[NoteStats] fetchNoteStats: deferred (already processing same id)', {
eventId: idShort,
kind: event.kind
})
return
}
@ -235,15 +220,6 @@ class NoteStatsService { @@ -235,15 +220,6 @@ class NoteStatsService {
}
rememberRoot()
logger.debug('[NoteStats] fetchNoteStats: queued new id', {
eventId: idShort,
kind: event.kind,
foreground,
pendingForeground: this.pendingForeground.size,
pendingBackground: this.pendingEvents.size,
immediateBatch: this.statsPendingSize() >= this.MAX_BATCH_SIZE
})
this.maybeFlushStatsBatch(foreground)
}
@ -276,20 +252,12 @@ class NoteStatsService { @@ -276,20 +252,12 @@ class NoteStatsService {
return
}
if (this.processBatchRunning) {
logger.debug('[NoteStats] processBatch: skipped (already running)', {
pendingForeground: this.pendingForeground.size,
pendingBackground: this.pendingEvents.size
})
return
}
if (this.pendingForeground.size === 0 && this.pendingEvents.size === 0) {
return
}
logger.debug('[NoteStats] processBatch: running', {
pendingForeground: this.pendingForeground.size,
pendingBackground: this.pendingEvents.size
})
this.processBatchRunning = true
if (this.batchTimeout) {
clearTimeout(this.batchTimeout)
@ -298,13 +266,6 @@ class NoteStatsService { @@ -298,13 +266,6 @@ class NoteStatsService {
try {
const eventsToProcess = this.takeNextStatsSlice()
logger.debug('[NoteStats] processBatch slice', {
count: eventsToProcess.length,
ids: eventsToProcess.map((id) => `${id.slice(0, 12)}`),
remainingForeground: this.pendingForeground.size,
remainingBackground: this.pendingEvents.size,
concurrency: this.STATS_SLICE_CONCURRENCY
})
for (let i = 0; i < eventsToProcess.length; i += this.STATS_SLICE_CONCURRENCY) {
const chunk = eventsToProcess.slice(i, i + this.STATS_SLICE_CONCURRENCY)
await Promise.all(chunk.map((eventId) => this.processSingleEvent(eventId)))
@ -319,9 +280,6 @@ class NoteStatsService { @@ -319,9 +280,6 @@ class NoteStatsService {
private async processSingleEvent(eventId: string) {
if (this.processingCache.has(eventId)) {
logger.debug('[NoteStats] processSingleEvent: skip (concurrent in-flight)', {
eventId: `${eventId.slice(0, 12)}`
})
return
}
@ -331,7 +289,7 @@ class NoteStatsService { @@ -331,7 +289,7 @@ class NoteStatsService {
this.pendingFetchFavoriteRelays.delete(eventId)
let publishedStatsSnapshot = false
const markStatsLoaded = (rawStatsKey: string, reason: string) => {
const markStatsLoaded = (rawStatsKey: string) => {
if (publishedStatsSnapshot) return
publishedStatsSnapshot = true
const statsKey = this.statsKey(rawStatsKey)
@ -339,35 +297,19 @@ class NoteStatsService { @@ -339,35 +297,19 @@ class NoteStatsService {
...(this.noteStatsMap.get(statsKey) ?? {}),
updatedAt: dayjs().unix()
})
const subscriberCount = this.noteStatsSubscribers.get(statsKey)?.size ?? 0
logger.debug('[NoteStats] processSingleEvent: snapshot published', {
statsKey: `${statsKey.slice(0, 12)}`,
reason,
subscriberCount
})
this.notifyNoteStats(statsKey)
}
let resolvedEvent: Event | undefined
try {
logger.debug('[NoteStats] processSingleEvent: start', { eventId: `${eventId.slice(0, 12)}` })
// Synthetic RSS/Web thread parents are not published; use the instance from fetchNoteStats.
const synthetic = this.pendingSyntheticRootById.get(eventId)
this.pendingSyntheticRootById.delete(eventId)
const callerRoot = this.pendingStatsRootEventById.get(eventId)
this.pendingStatsRootEventById.delete(eventId)
resolvedEvent = synthetic ?? callerRoot ?? (await eventService.fetchEvent(eventId))
const rootSource = synthetic ? 'synthetic-rss' : callerRoot ? 'caller-card' : resolvedEvent ? 'fetchEvent' : 'none'
logger.debug('[NoteStats] processSingleEvent: root resolution', {
eventId: `${eventId.slice(0, 12)}`,
rootSource,
resolvedKind: resolvedEvent?.kind
})
if (!resolvedEvent) {
logger.debug('[NoteStats] processSingleEvent: no root event — publishing empty snapshot', {
eventId: `${eventId.slice(0, 12)}`
})
markStatsLoaded(eventId, 'no-root-event')
markStatsLoaded(eventId)
return
}
@ -379,10 +321,6 @@ class NoteStatsService { @@ -379,10 +321,6 @@ class NoteStatsService {
this.updateNoteStatsByEvents(preFromSession, resolvedEvent.pubkey, {
statsRootEvent: resolvedEvent
})
logger.debug('[NoteStats] processSingleEvent: pre-merged session interactions', {
eventId: `${resolvedEvent.id.slice(0, 12)}`,
count: preFromSession.length
})
}
}
@ -399,21 +337,11 @@ class NoteStatsService { @@ -399,21 +337,11 @@ class NoteStatsService {
firstRelayResultGraceMs: false as const
}
const events: Event[] = []
logger.debug(
'[NoteStats] Fetching stats for event',
resolvedEvent.id.substring(0, 8),
'from',
finalRelayUrls.length,
'relays'
)
const { queryService } = await import('@/services/client.service')
const onStatsEvent = (evt: Event) => {
this.updateNoteStatsByEvents([evt], resolvedEvent!.pubkey, {
statsRootEvent: resolvedEvent!
})
events.push(evt)
}
await Promise.all([
nonSocial.length > 0
@ -430,21 +358,16 @@ class NoteStatsService { @@ -430,21 +358,16 @@ class NoteStatsService {
: Promise.resolve([] as Event[])
])
logger.debug('[NoteStats] processSingleEvent: relay fetch finished', {
eventId: `${resolvedEvent.id.slice(0, 12)}`,
interactionEventsReceived: events.length
})
markStatsLoaded(resolvedEvent.id, 'fetch-ok')
markStatsLoaded(resolvedEvent.id)
} catch (err) {
logger.warn('[NoteStats] processSingleEvent failed', {
eventId: eventId.substring(0, 8),
error: err instanceof Error ? err.message : String(err)
})
markStatsLoaded(resolvedEvent?.id ?? eventId, 'catch-after-error')
markStatsLoaded(resolvedEvent?.id ?? eventId)
} finally {
if (!publishedStatsSnapshot) {
markStatsLoaded(resolvedEvent?.id ?? eventId, 'finally-fallback')
markStatsLoaded(resolvedEvent?.id ?? eventId)
}
this.processingCache.delete(eventId)
if (this.inFlightDeferredFavoriteRelays.has(eventId)) {

Loading…
Cancel
Save