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. 220
      src/services/client-replaceable-events.service.ts
  7. 23
      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({
let content: React.ReactNode let content: React.ReactNode
if (!isRenderableNoteKind(event.kind)) { if (!isRenderableNoteKind(event.kind)) {
logger.debug('Note component - rendering UnknownNote for unsupported kind:', event.kind)
content = <UnknownNote className="mt-2" event={displayEvent} omitKindLabel /> content = <UnknownNote className="mt-2" event={displayEvent} omitKindLabel />
} else if (muteSetHas(mutePubkeySet, event.pubkey) && !showMuted) { } else if (muteSetHas(mutePubkeySet, event.pubkey) && !showMuted) {
content = <MutedNote show={() => setShowMuted(true)} /> content = <MutedNote show={() => setShowMuted(true)} />

88
src/components/SearchResult/FullTextSearchByRelay.tsx

@ -2,7 +2,6 @@ import NoteCard from '@/components/NoteCard'
import RelayIcon from '@/components/RelayIcon' import RelayIcon from '@/components/RelayIcon'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { compareEventsForDTagQuery } from '@/lib/dtag-search' import { compareEventsForDTagQuery } from '@/lib/dtag-search'
import logger from '@/lib/logger'
import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey' import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey'
import { normalizeUrl } from '@/lib/url' import { normalizeUrl } from '@/lib/url'
import { NoteFeedProfileContext, type NoteFeedProfileContextValue } from '@/providers/NoteFeedProfileContext' import { NoteFeedProfileContext, type NoteFeedProfileContextValue } from '@/providers/NoteFeedProfileContext'
@ -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({ export default function FullTextSearchByRelay({
searchQuery, searchQuery,
relayUrls, relayUrls,
@ -325,15 +306,6 @@ export default function FullTextSearchByRelay({
} }
const runOneRelay = async (relayUrl: string) => { 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() const t0 = performance.now()
try { try {
const { events: raw, connectionError } = await client.fetchEventsFromSingleRelay( const { events: raw, connectionError } = await client.fetchEventsFromSingleRelay(
@ -350,18 +322,6 @@ export default function FullTextSearchByRelay({
if (myRun !== runGeneration.current) return if (myRun !== runGeneration.current) return
const ms = Math.round(performance.now() - t0) const ms = Math.round(performance.now() - t0)
if (sorted.length === 0 && connectionError) { 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) => setRelayRows((prev) =>
prev.map((r) => prev.map((r) =>
r.relayUrl === relayUrl r.relayUrl === relayUrl
@ -374,28 +334,6 @@ export default function FullTextSearchByRelay({
mergeIntoHits(relayUrl, sorted) 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) => setRelayRows((prev) =>
prev.map((r) => prev.map((r) =>
r.relayUrl === relayUrl r.relayUrl === relayUrl
@ -413,18 +351,6 @@ export default function FullTextSearchByRelay({
if (myRun !== runGeneration.current) return if (myRun !== runGeneration.current) return
const msg = err instanceof Error ? err.message : String(err) const msg = err instanceof Error ? err.message : String(err)
const ms = Math.round(performance.now() - t0) 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) => setRelayRows((prev) =>
prev.map((r) => prev.map((r) =>
r.relayUrl === relayUrl ? { ...r, phase: 'error', eventCount: 0, ms, errorMessage: msg } : r r.relayUrl === relayUrl ? { ...r, phase: 'error', eventCount: 0, ms, errorMessage: msg } : r
@ -442,25 +368,11 @@ export default function FullTextSearchByRelay({
} }
void (async () => { 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 { try {
await Promise.all(Array.from({ length: poolSize }, () => worker())) await Promise.all(Array.from({ length: poolSize }, () => worker()))
} catch { } catch {
/* runOneRelay already updates relay rows */ /* 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 return cleanupInvalidatePreviousRun

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

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

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

@ -503,34 +503,12 @@ export class EventService {
filter.ids?.length === 1 && /^[0-9a-f]{64}$/i.test(String(filter.ids[0])) filter.ids?.length === 1 && /^[0-9a-f]{64}$/i.test(String(filter.ids[0]))
? { explicitNoteLookupHexId: String(filter.ids[0]).toLowerCase() } ? { explicitNoteLookupHexId: String(filter.ids[0]).toLowerCase() }
: undefined : 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. */ /** 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, { const events = await this.queryService.query(externalRelays, filter, undefined, {
eoseTimeout: 12_000, eoseTimeout: 12_000,
globalTimeout: 35_000, globalTimeout: 35_000,
immediateReturn: false 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 const usable = events
.filter((e) => !shouldDropEventOnIngest(e, ingestOpts)) .filter((e) => !shouldDropEventOnIngest(e, ingestOpts))
@ -1249,13 +1227,6 @@ export class EventService {
return undefined 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) const isSingleEventById = Boolean(filter.ids && filter.ids.length === 1 && filter.limit === 1)
/** Replaceable coordinate: `#a` (preferred) or legacy `authors` + `#d`. */ /** Replaceable coordinate: `#a` (preferred) or legacy `authors` + `#d`. */
const isReplaceableCoordinateFetch = const isReplaceableCoordinateFetch =
@ -1275,13 +1246,6 @@ export class EventService {
.filter((e) => !shouldDropEventOnIngest(e, ingestOpts)) .filter((e) => !shouldDropEventOnIngest(e, ingestOpts))
.sort((a, b) => b.created_at - a.created_at)[0] .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 return event
} }

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

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

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

@ -186,13 +186,6 @@ export class ReplaceableEventService {
containingEventRelays: string[] = [] containingEventRelays: string[] = []
): Promise<NEvent | undefined> { ): Promise<NEvent | undefined> {
const cacheKey = d ? `${kind}:${pubkey}:${d}` : `${kind}:${pubkey}` const cacheKey = d ? `${kind}:${pubkey}:${d}` : `${kind}:${pubkey}`
logger.debug('[ReplaceableEventService] fetchReplaceableEvent start', {
pubkey,
kind,
d,
cacheKey,
containingEventRelays: containingEventRelays.length
})
try { try {
if (kind === kinds.Metadata && !d) { if (kind === kinds.Metadata && !d) {
@ -212,9 +205,6 @@ export class ReplaceableEventService {
containingEventRelays.length === 0 && containingEventRelays.length === 0 &&
ReplaceableEventService.isProfileFetchMissCached(pubkey) ReplaceableEventService.isProfileFetchMissCached(pubkey)
) { ) {
logger.debug('[ReplaceableEventService] Skipping metadata fetch (recent profile miss cache)', {
pubkey
})
return undefined return undefined
} }
@ -247,18 +237,9 @@ export class ReplaceableEventService {
let event: NEvent | undefined let event: NEvent | undefined
if (containingEventRelays.length > 0 && kind === kinds.Metadata && !d) { if (containingEventRelays.length > 0 && kind === kinds.Metadata && !d) {
// For profiles with containing event relays (author's relay list), check IndexedDB first, then query directly // 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 { try {
const indexedDbCached = await indexedDb.getReplaceableEvent(pubkey, kind, d) const indexedDbCached = await indexedDb.getReplaceableEvent(pubkey, kind, d)
if (indexedDbCached) { if (indexedDbCached) {
logger.debug('[ReplaceableEventService] Found in IndexedDB', {
pubkey,
kind,
eventId: indexedDbCached.id
})
// Refresh in background // Refresh in background
this.refreshInBackground(pubkey, kind, d).catch(() => {}) this.refreshInBackground(pubkey, kind, d).catch(() => {})
return indexedDbCached return indexedDbCached
@ -272,17 +253,7 @@ export class ReplaceableEventService {
} }
// Not in IndexedDB, fetch from network with custom relay list // 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, []) 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( const events = await this.queryService.query(
relayUrls, relayUrls,
{ {
@ -296,51 +267,19 @@ export class ReplaceableEventService {
globalTimeout: METADATA_BATCH_QUERY_GLOBAL_TIMEOUT_MS 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) const sortedEvents = events.sort((a, b) => b.created_at - a.created_at)
event = sortedEvents.length > 0 ? sortedEvents[0] : undefined event = sortedEvents.length > 0 ? sortedEvents[0] : undefined
} else { } else {
// Use DataLoader for batching (IndexedDB checks and network fetches are batched) // 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 const loadedEvent = d
? await this.replaceableEventDataLoader.load({ pubkey, kind, d }) ? await this.replaceableEventDataLoader.load({ pubkey, kind, d })
: await this.replaceableEventFromBigRelaysDataloader.load({ pubkey, kind }) : 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 event = loadedEvent || undefined
} }
if (event) { if (event) {
logger.debug('[ReplaceableEventService] Event found', {
pubkey,
kind,
eventId: event.id,
created_at: event.created_at
})
return event 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) { } catch (error) {
// Log errors but don't throw - return undefined so UI can show fallback // Log errors but don't throw - return undefined so UI can show fallback
if (kind === kinds.Metadata) { if (kind === kinds.Metadata) {
@ -358,10 +297,6 @@ export class ReplaceableEventService {
} }
} }
logger.debug('[ReplaceableEventService] fetchReplaceableEvent returning undefined', {
pubkey,
kind
})
return undefined return undefined
} }
@ -486,19 +421,6 @@ export class ReplaceableEventService {
private async replaceableEventFromBigRelaysBatchLoadFn( private async replaceableEventFromBigRelaysBatchLoadFn(
params: readonly { pubkey: string; kind: number }[] params: readonly { pubkey: string; kind: number }[]
): Promise<(NEvent | null)[]> { ): 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 results: (NEvent | null)[] = new Array(params.length).fill(null)
const eventsMap = new Map<string, NEvent>() const eventsMap = new Map<string, NEvent>()
@ -532,11 +454,6 @@ export class ReplaceableEventService {
const pubkeys = items.map((x) => x.pubkey) const pubkeys = items.map((x) => x.pubkey)
try { try {
const indexedDbEvents = await indexedDb.getManyReplaceableEvents(pubkeys, kind) 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) => { items.forEach(({ pubkey, index }, idx) => {
const event = indexedDbEvents[idx] const event = indexedDbEvents[idx]
@ -581,9 +498,6 @@ export class ReplaceableEventService {
// Step 2: Only fetch missing events from network // Step 2: Only fetch missing events from network
if (missingParams.length === 0) { if (missingParams.length === 0) {
logger.debug('[ReplaceableEventService] All events resolved (session + IndexedDB), skipping network fetch', {
totalCount: params.length
})
return results return results
} }
@ -604,19 +518,6 @@ export class ReplaceableEventService {
} }
if (networkMissing.length > 0) { 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 // Group missing params by kind for network fetch
const missingGroups = new Map<number, { pubkey: string; index: number }[]>() const missingGroups = new Map<number, { pubkey: string; index: number }[]>()
networkMissing.forEach(({ pubkey, kind, index }) => { networkMissing.forEach(({ pubkey, kind, index }) => {
@ -714,21 +615,6 @@ export class ReplaceableEventService {
} else { } else {
relayUrls = [...FAST_READ_RELAY_URLS] 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 // 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). // and multi-author batches must not use replaceableRace (first EVENT may not be the latest per author).
const isSlowReplaceableBatch = const isSlowReplaceableBatch =
@ -778,20 +664,6 @@ export class ReplaceableEventService {
queryOpts 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 // 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 we have too many events, only process the most recent ones per pubkey
@ -812,10 +684,6 @@ export class ReplaceableEventService {
} }
// Convert back to array, but limit to reasonable size // Convert back to array, but limit to reasonable size
const limitedEvents = Array.from(eventsByPubkey.values()).slice(0, 500) 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 // Use limited events for processing
for (const event of limitedEvents) { for (const event of limitedEvents) {
const key = `${event.pubkey}:${event.kind}` const key = `${event.pubkey}:${event.kind}`
@ -864,21 +732,8 @@ export class ReplaceableEventService {
} }
} }
// 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 // Step 3: Persist hits only. Do not write negative cache rows (`value: null`) — optional kinds
@ -893,20 +748,6 @@ 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 return results
} }
@ -1000,17 +841,13 @@ export class ReplaceableEventService {
* Fetch profile event by id (hex, npub, nprofile) * Fetch profile event by id (hex, npub, nprofile)
*/ */
async fetchProfileEvent(id: string, _skipCache: boolean = false): Promise<NEvent | undefined> { async fetchProfileEvent(id: string, _skipCache: boolean = false): Promise<NEvent | undefined> {
logger.debug('[ReplaceableEventService] fetchProfileEvent start', { id })
let pubkey: string | undefined let pubkey: string | undefined
let relays: string[] = [] let relays: string[] = []
if (/^[0-9a-f]{64}$/.test(id)) { if (/^[0-9a-f]{64}$/.test(id)) {
pubkey = id pubkey = id
logger.debug('[ReplaceableEventService] ID is hex pubkey', { pubkey })
} else { } else {
try { try {
const { data, type } = nip19.decode(id) const { data, type } = nip19.decode(id)
logger.debug('[ReplaceableEventService] Decoded bech32 ID', { type })
switch (type) { switch (type) {
case 'npub': case 'npub':
pubkey = data pubkey = data
@ -1018,7 +855,6 @@ export class ReplaceableEventService {
case 'nprofile': case 'nprofile':
pubkey = data.pubkey pubkey = data.pubkey
if (data.relays) relays = data.relays if (data.relays) relays = data.relays
logger.debug('[ReplaceableEventService] nprofile has relay hints', { relayCount: relays.length })
break break
} }
} catch (error) { } catch (error) {
@ -1062,21 +898,11 @@ export class ReplaceableEventService {
// CRITICAL: Do NOT pass relay hints here - passing any relays bypasses DataLoader and creates individual subscriptions // 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 // 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 // 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 // fetchReplaceableEvent uses DataLoader which checks IndexedDB first, then queries default relays
// Passing empty array ensures DataLoader is used (batched) - this prevents individual subscriptions // Passing empty array ensures DataLoader is used (batched) - this prevents individual subscriptions
const profileEvent = await this.fetchReplaceableEvent(pubkey, kinds.Metadata, undefined, []) const profileEvent = await this.fetchReplaceableEvent(pubkey, kinds.Metadata, undefined, [])
if (profileEvent) { if (profileEvent) {
logger.debug('[ReplaceableEventService] Profile found via cache / default relays (DataLoader)', {
pubkey,
eventId: profileEvent.id
})
await this.indexProfile(profileEvent) await this.indexProfile(profileEvent)
return profileEvent return profileEvent
} }
@ -1084,24 +910,15 @@ export class ReplaceableEventService {
await ReplaceableEventService.acquireProfileFallbackNetworkSlot() await ReplaceableEventService.acquireProfileFallbackNetworkSlot()
try { try {
// Step 2: Only after cache + default relays miss — NIP-65 relay list (timeout-capped), then hints + outbox/inbox + defaults. // 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 let authorRelayList: { read?: string[]; write?: string[] } | null = null
try { try {
const hasLocal10002 = await ReplaceableEventService.hasRelayListInLocalCache(pubkey) const hasLocal10002 = await ReplaceableEventService.hasRelayListInLocalCache(pubkey)
if (hasLocal10002) { if (hasLocal10002) {
authorRelayList = await client.peekRelayListFromStorage(pubkey) authorRelayList = await client.peekRelayListFromStorage(pubkey)
logger.debug('[ReplaceableEventService] Step 2: using cached kind 10002 (skip fetchRelayList network)', {
pubkey
})
} else { } else {
const relayListPromise = client.fetchRelayList(pubkey) const relayListPromise = client.fetchRelayList(pubkey)
const timeoutPromise = new Promise<null>((resolve) => { const timeoutPromise = new Promise<null>((resolve) => {
setTimeout(() => { setTimeout(() => {
logger.debug('[ReplaceableEventService] fetchRelayList timeout, giving up', { pubkey })
resolve(null) resolve(null)
}, 2800) }, 2800)
}) })
@ -1140,16 +957,11 @@ export class ReplaceableEventService {
expandedRelays expandedRelays
) )
if (profileFromExpanded) { if (profileFromExpanded) {
logger.debug('[ReplaceableEventService] Profile found after relay-list fallback', {
pubkey,
eventId: profileFromExpanded.id
})
await this.indexProfile(profileFromExpanded) await this.indexProfile(profileFromExpanded)
return profileFromExpanded return profileFromExpanded
} }
// Step 3: Last resort — broad relay query (timeout-bounded in query layer) // Step 3: Last resort — broad relay query (timeout-bounded in query layer)
logger.debug('[ReplaceableEventService] Step 3: Comprehensive relay query (last resort)', { pubkey })
try { try {
const userPubkey = client.pubkey const userPubkey = client.pubkey
const comprehensiveRelays = await buildComprehensiveRelayList({ const comprehensiveRelays = await buildComprehensiveRelayList({
@ -1165,14 +977,7 @@ export class ReplaceableEventService {
includeLocalRelays: true includeLocalRelays: true
}) })
logger.debug('[ReplaceableEventService] Comprehensive relay list built', {
pubkey,
relayCount: comprehensiveRelays.length,
relays: comprehensiveRelays.slice(0, 10)
})
if (comprehensiveRelays.length > 0) { if (comprehensiveRelays.length > 0) {
const startTime = Date.now()
const events = await this.queryService.query( const events = await this.queryService.query(
comprehensiveRelays, comprehensiveRelays,
{ {
@ -1186,22 +991,10 @@ export class ReplaceableEventService {
globalTimeout: 3500 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) { if (events.length > 0) {
const sortedEvents = events.sort((a, b) => b.created_at - a.created_at) const sortedEvents = events.sort((a, b) => b.created_at - a.created_at)
const found = sortedEvents[0]! const found = sortedEvents[0]!
logger.debug('[ReplaceableEventService] Profile found via comprehensive search', {
pubkey,
eventId: found.id
})
await this.indexProfile(found) await this.indexProfile(found)
return found return found
} }
@ -1216,10 +1009,6 @@ export class ReplaceableEventService {
ReplaceableEventService.releaseProfileFallbackNetworkSlot() 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) { if (!_skipCache && relayHints.length === 0) {
ReplaceableEventService.rememberProfileFetchMiss(pubkey) ReplaceableEventService.rememberProfileFetchMiss(pubkey)
} }
@ -1676,15 +1465,6 @@ export class ReplaceableEventService {
result.push([relayUrl, Array.from(pubkeys)]) 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 return result
} }
} }

23
src/services/client.service.ts

@ -3941,23 +3941,12 @@ class ClientService extends EventTarget {
const cacheKey = this.relayListRequestCacheKey(pubkey) const cacheKey = this.relayListRequestCacheKey(pubkey)
const existingRequest = this.relayListRequestCache.get(cacheKey) const existingRequest = this.relayListRequestCache.get(cacheKey)
if (existingRequest) { if (existingRequest) {
// Leader already logged `[FetchRelayList] Starting fetch`; joiners stay silent per burst.
return existingRequest return existingRequest
} }
logger.debug('[FetchRelayList] Starting fetch', { pubkey })
const requestPromise = (async () => { const requestPromise = (async () => {
try { try {
const startTime = Date.now()
const [relayList] = await this.fetchRelayLists([pubkey]) 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 return relayList
} catch (error) { } catch (error) {
logger.warn('[FetchRelayList] Fetch failed; using IndexedDB / defaults', { logger.warn('[FetchRelayList] Fetch failed; using IndexedDB / defaults', {
@ -4213,14 +4202,6 @@ class ClientService extends EventTarget {
const missing10002Pubkeys = pubkeys.filter((_pk, i) => storedRelayEvents[i] == null) const missing10002Pubkeys = pubkeys.filter((_pk, i) => storedRelayEvents[i] == null)
if (missing10002Pubkeys.length > 0) { 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([ const [relFetched, httpFetched] = await Promise.all([
this.replaceableEventService.fetchReplaceableEventsFromProfileFetchRelays( this.replaceableEventService.fetchReplaceableEventsFromProfileFetchRelays(
missing10002Pubkeys, missing10002Pubkeys,
@ -4269,10 +4250,6 @@ class ClientService extends EventTarget {
if (allHaveKind10002) { if (allHaveKind10002) {
this.refreshRelayListsFromNetwork(pubkeys, storedRelayEvents) 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([ const cacheRelayEvents = await Promise.race([
this.fetchCacheRelayEventsFromMultipleSources(pubkeys, storedRelayEvents, storedRelayEvents), this.fetchCacheRelayEventsFromMultipleSources(pubkeys, storedRelayEvents, storedRelayEvents),
new Promise<(NEvent | null | undefined)[]>((resolve) => new Promise<(NEvent | null | undefined)[]>((resolve) =>

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

@ -270,29 +270,6 @@ class IndexedDbService {
private static readonly TOMBSTONE_NOT_CACHE_TTL_MS = 45_000 private static readonly TOMBSTONE_NOT_CACHE_TTL_MS = 45_000
private static readonly TOMBSTONE_NOT_CACHE_MAX = 4096 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 private static readonly CLEANUP_INITIAL_DELAY_MS = 60 * 1000
/** Repeat TTL sweeps on this interval so pruning is not a one-shot. */ /** Repeat TTL sweeps on this interval so pruning is not a one-shot. */
private static readonly CLEANUP_INTERVAL_MS = 60 * 60 * 1000 private static readonly CLEANUP_INTERVAL_MS = 60 * 60 * 1000
@ -624,16 +601,8 @@ class IndexedDbService {
const request = store.get(key) const request = store.get(key)
request.onsuccess = () => { request.onsuccess = () => {
const allowDetailLog = this.takeReplaceableGetDebugLogSlot()
const row = request.result as TValue<Event> | undefined const row = request.result as TValue<Event> | undefined
if (!row) { if (!row) {
if (allowDetailLog) {
logger.debug('[IndexedDB] getReplaceableEvent - no row found', {
pubkey,
kind,
d
})
}
transaction.commit() transaction.commit()
return resolve(undefined) return resolve(undefined)
} }
@ -644,22 +613,6 @@ class IndexedDbService {
if (isProfileOrPayment && row.addedAt && Date.now() - row.addedAt > PROFILE_AND_PAYMENT_CACHE_MAX_AGE_MS) { 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 // 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 // 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() transaction.commit()
resolve(row.value) resolve(row.value)

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

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

Loading…
Cancel
Save