diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx index eaaec1d7..9446532e 100644 --- a/src/components/Note/index.tsx +++ b/src/components/Note/index.tsx @@ -318,6 +318,9 @@ export default function Note({ hideMetadata?: boolean className?: string } = {}) => { + if (isNip18RepostKind(displayEvent.kind)) { + return + } const embeddedEvent = findTrailingStringifiedNostrEvent(displayEvent.content ?? '') if (embeddedEvent) { return ( @@ -333,6 +336,9 @@ export default function Note({ ) } if (isStringifiedJsonContent(displayEvent.content)) { + if (isNip18RepostKind(displayEvent.kind)) { + return + } return (
 setShowNsfw(true)} />
   } else if (isNip25ReactionKind(event.kind)) {
     content = null
-  } else if (isNip18RepostKind(event.kind)) {
+  } else if (isNip18RepostKind(displayEvent.kind)) {
     content = 
   } else if (event.kind === ExtendedKind.POLL_RESPONSE) {
     content = 
diff --git a/src/lib/nostr-relay-auth-patch.ts b/src/lib/nostr-relay-auth-patch.ts
index bec0248e..327bb3be 100644
--- a/src/lib/nostr-relay-auth-patch.ts
+++ b/src/lib/nostr-relay-auth-patch.ts
@@ -1,4 +1,3 @@
-import logger from '@/lib/logger'
 import { notifyRelayNip42Accepted, notifyRelayNip42Rejected } from '@/lib/relay-auth-feedback'
 import type { AbstractRelay } from 'nostr-tools/abstract-relay'
 import type { EventTemplate, VerifiedEvent } from 'nostr-tools'
@@ -65,9 +64,6 @@ export function patchPoolRelayAuthRaceAndFeedback(relay: object): void {
     const r = asRelayInternals(this)
     if (!r.connectionPromise && typeof message === 'string' && message.startsWith('["AUTH"')) {
       abortPendingAuthForDeadSocket(r, message)
-      logger.debug('[RelayOp] Dropped AUTH (socket already closed; connect timeout vs signing race)', {
-        url: r.url
-      })
       return Promise.resolve()
     }
     return origSend.call(this, message) as Promise
@@ -91,7 +87,6 @@ export function patchPoolRelayAuthRaceAndFeedback(relay: object): void {
           msg.includes('relay connection closed before AUTH') ||
           /relay connection closed/i.test(msg)
         if (benignRace) {
-          logger.debug('[RelayOp] Relay AUTH aborted (benign race)', { url: r.url, detail: msg })
           r.authPromise = undefined
           return ''
         }
diff --git a/src/lib/relay-strikes.ts b/src/lib/relay-strikes.ts
index c9e28dbe..460ce74a 100644
--- a/src/lib/relay-strikes.ts
+++ b/src/lib/relay-strikes.ts
@@ -65,8 +65,6 @@ function sessionKey(url: string): string {
 class RelaySessionStrikes {
   private byKey = new Map()
   private cacheRelayKeys = new Set()
-  /** Throttle debug spam when many parallel REQs hit the same dead relay (cache rows bypass strike debounce). */
-  private lastReadFailureDebugLogAt = new Map()
 
   setSessionCacheRelayKeysFromKind10432(ev: Event | null | undefined): void {
     this.cacheRelayKeys.clear()
@@ -136,13 +134,8 @@ class RelaySessionStrikes {
   }
 
   private applyRateLimitCooldownKey(key: string): void {
-    const now = Date.now()
     const e = this.getEntry(key)
-    e.rateLimitUntil = Math.max(e.rateLimitUntil, now + RATE_LIMIT_COOLDOWN_MS)
-    logger.debug('[RelayStrikes] rate-limit cooldown', {
-      key,
-      untilMs: e.rateLimitUntil - now
-    })
+    e.rateLimitUntil = Math.max(e.rateLimitUntil, Date.now() + RATE_LIMIT_COOLDOWN_MS)
   }
 
   /** WS connect failure, HTTP transport failure, etc. */
@@ -159,8 +152,7 @@ class RelaySessionStrikes {
     // Cache relays from kind 10432 (e.g. localhost on another machine) always accrue failures.
     if (now < e.rateLimitUntil && !this.cacheRelayKeys.has(key)) return
 
-    const cache = this.cacheRelayKeys.has(key)
-    if (!cache) {
+    if (!this.cacheRelayKeys.has(key)) {
       if (now - e.readLastStrikeIncrementAt < STRIKE_INCREMENT_DEBOUNCE_MS) return
       e.readLastStrikeIncrementAt = now
     }
@@ -168,13 +160,7 @@ class RelaySessionStrikes {
     e.readFailures += 1
     if (e.readFailures >= STRIKE_FAILURES_THRESHOLD) {
       e.readStrikeSkipUntil = Math.max(e.readStrikeSkipUntil, now + STRIKE_COOLDOWN_MS)
-      logger.info('[RelayStrikes] read path strike skip', { key, readFailures: e.readFailures })
-    } else {
-      const lastDbg = this.lastReadFailureDebugLogAt.get(key) ?? 0
-      if (now - lastDbg >= STRIKE_INCREMENT_DEBOUNCE_MS) {
-        this.lastReadFailureDebugLogAt.set(key, now)
-        logger.debug('[RelayStrikes] read failure counted', { key, readFailures: e.readFailures, cache })
-      }
+      logger.warn('[RelayStrikes] read path strike skip', { key, readFailures: e.readFailures })
     }
   }
 
@@ -186,7 +172,6 @@ class RelaySessionStrikes {
     e.readFailures = 0
     e.readStrikeSkipUntil = 0
     e.readLastStrikeIncrementAt = 0
-    this.lastReadFailureDebugLogAt.delete(key)
   }
 
   recordPublishFailure(url: string): void {
@@ -195,15 +180,14 @@ class RelaySessionStrikes {
     const now = Date.now()
     const e = this.getEntry(key)
     if (now < e.rateLimitUntil && !this.cacheRelayKeys.has(key)) return
-    const cache = this.cacheRelayKeys.has(key)
-    if (!cache) {
+    if (!this.cacheRelayKeys.has(key)) {
       if (now - e.publishLastStrikeIncrementAt < STRIKE_INCREMENT_DEBOUNCE_MS) return
       e.publishLastStrikeIncrementAt = now
     }
     e.publishFailures += 1
     if (e.publishFailures >= STRIKE_FAILURES_THRESHOLD) {
       e.publishStrikeSkipUntil = Math.max(e.publishStrikeSkipUntil, now + STRIKE_COOLDOWN_MS)
-      logger.info('[RelayStrikes] publish path strike skip', { key, publishFailures: e.publishFailures })
+      logger.warn('[RelayStrikes] publish path strike skip', { key, publishFailures: e.publishFailures })
     }
   }
 
@@ -248,7 +232,6 @@ class RelaySessionStrikes {
   reset(): void {
     this.byKey.clear()
     this.cacheRelayKeys.clear()
-    this.lastReadFailureDebugLogAt.clear()
   }
 }
 
diff --git a/src/services/client-query.service.ts b/src/services/client-query.service.ts
index c9c4e95b..14deb2aa 100644
--- a/src/services/client-query.service.ts
+++ b/src/services/client-query.service.ts
@@ -28,7 +28,7 @@ import {
 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 { canonicalRelaySessionKey, isHttpRelayUrl, normalizeHttpRelayUrl, normalizeUrl } from '@/lib/url'
 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'
@@ -405,11 +405,14 @@ export class QueryService {
     /** One chunk → pass a single Filter (compat); several (e.g. kinds split) → full array for WS + HTTP. */
     const effectiveFilter: Filter | Filter[] =
       sanitizedFilters.length === 1 ? sanitizedFilters[0]! : sanitizedFilters
-    const eoseTimeout = options?.eoseTimeout ?? 500
     const hasNip50Search = filtersHaveNip50Search(sanitizedFilters)
-    const globalTimeoutRaw = options?.globalTimeout ?? 10000
     const useNip50QueryTimeoutFloor =
       hasNip50Search && options?.relayOpSource === 'fetchEventsFromSingleRelay'
+    /** After all relays EOSE, wait this long before closing so slow `EVENT` tails are not cut off (NIP-50 is heavy). */
+    const eoseTimeout = useNip50QueryTimeoutFloor
+      ? Math.max(options?.eoseTimeout ?? 500, 3_000)
+      : options?.eoseTimeout ?? 500
+    const globalTimeoutRaw = options?.globalTimeout ?? 10000
     const globalTimeout = useNip50QueryTimeoutFloor
       ? Math.max(globalTimeoutRaw, NIP50_QUERY_GLOBAL_TIMEOUT_FLOOR_MS)
       : globalTimeoutRaw
@@ -733,7 +736,7 @@ export class QueryService {
     relayOpMeta?: {
       source: string
       logLevel?: 'info' | 'debug'
-      /** When true, suppress `[RelayOp] batch_begin` / `batch_end` (used by {@link QueryService.query}). */
+      /** When true (default on batches), suppress `[RelayOp] batch_begin` / `batch_end`. */
       quiet?: boolean
       onBatchEnd?: (rows: RelayOpTerminalRow[]) => void
     }
@@ -780,19 +783,27 @@ export class QueryService {
       grouped.get(key)!.push(...filters)
     }
     
-    const searchableSet = new Set([
-      ...SEARCHABLE_RELAY_URLS.map((u) => normalizeUrl(u) || u),
-      ...nip66Service.getSearchableRelayUrls().map((u) => normalizeUrl(u) || u),
-      ...PROFILE_RELAY_URLS.map((u) => normalizeUrl(u) || u).filter(Boolean)
-    ])
-    
+    const searchableSet = new Set(
+      [
+        ...SEARCHABLE_RELAY_URLS,
+        ...nip66Service.getSearchableRelayUrls(),
+        ...PROFILE_RELAY_URLS
+      ]
+        .map((u) => canonicalRelaySessionKey(normalizeUrl(u) || String(u).trim()))
+        .filter((k): k is string => k.length > 0)
+    )
+
+    const hasNip50Search = filtersHaveNip50Search(filters)
+    /** Single-relay NIP-50 (e.g. search page / per-relay spell): caller targets one index relay — never strip `search`. */
+    const singleRelayNip50 = hasNip50Search && grouped.size === 1
+
     const groupedRequests = Array.from(grouped.entries()).map(([url, f]) => {
-      const relaySupportsSearch = searchableSet.has(url) || nip66Service.isRelaySearchable(url)
-      const filtersForRelay = f.map((one) => filterForRelay(one, relaySupportsSearch))
+      const sessionKey = canonicalRelaySessionKey(url)
+      const relaySupportsSearch =
+        searchableSet.has(sessionKey) || nip66Service.isRelaySearchable(url)
+      const filtersForRelay = singleRelayNip50 ? f : f.map((one) => filterForRelay(one, relaySupportsSearch))
       return { url, filters: filtersForRelay }
     })
-
-    const hasNip50Search = filtersHaveNip50Search(filters)
     const relaySubscriptionEoseTimeoutMs = hasNip50Search
       ? NIP50_RELAY_SUBSCRIPTION_EOSE_TIMEOUT_MS
       : 10_000
diff --git a/src/services/client.service.ts b/src/services/client.service.ts
index fb19920e..483946e7 100644
--- a/src/services/client.service.ts
+++ b/src/services/client.service.ts
@@ -2068,17 +2068,6 @@ class ClientService extends EventTarget {
   ) {
     const timelineBatchId = `tl-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 9)}`
     const timelineT0 = performance.now()
-    logger.debug('[RelayOp] timeline_wave_begin', {
-      timelineBatchId,
-      shardCount: subRequests.length,
-      relayCountsPerShard: subRequests.map((r) => r.urls.length),
-      shards: subRequests.map((s, shardIndex) => ({
-        shardIndex,
-        relayCount: s.urls.length,
-        relaysSample: [...new Set(s.urls.map((u) => normalizeUrl(u) || u))].slice(0, 40),
-        filter: compactFilterForRelayLog(s.filter as Filter)
-      }))
-    })
     logger.debug('[relay-req] timeline_batch_start', {
       timelineBatchId,
       subRequestCount: subRequests.length,
@@ -3243,6 +3232,8 @@ class ClientService extends EventTarget {
       globalTimeout: options?.globalTimeout ?? 25_000,
       relayOpSource: 'fetchEventsFromSingleRelay' as const,
       foreground: true as const,
+      /** NIP-50 must run to EOSE; implicit feed grace would close the REQ after the first hit. */
+      firstRelayResultGraceMs: false as const,
       ...(options?.signal ? { signal: options.signal } : {})
     }
 
diff --git a/src/services/relay-operation-log.service.ts b/src/services/relay-operation-log.service.ts
index 36c632b2..1a0b8fb3 100644
--- a/src/services/relay-operation-log.service.ts
+++ b/src/services/relay-operation-log.service.ts
@@ -139,9 +139,9 @@ function groupTerminalsByOutcome(rows: RelayOpTerminalRow[]): Record void
@@ -198,7 +198,7 @@ export class RelaySubscribeOpBatch {
     this.source = source
     this.grouped = grouped
     this.logLevel = options?.logLevel ?? 'debug'
-    this.quiet = options?.quiet ?? false
+    this.quiet = options?.quiet ?? true
     this.onBatchEnd = options?.onBatchEnd
   }
 
@@ -340,14 +340,7 @@ export class RelayPublishOpBatch {
   }
 
   logBegin(): void {
-    logger.debug('[RelayOp] publish_batch_begin', {
-      batchId: this.batchId,
-      source: this.source,
-      eventId: this.eventId,
-      relayCount: this.relays.length,
-      relays: this.relays,
-      commands: this.relays.map((relay, cmdIndex) => ({ cmdIndex, relay, eventId: this.eventId }))
-    })
+    /* Intentionally quiet: publish outcomes surface in UI; failures are logged in logEnd. */
   }
 
   record(cmdIndex: number, relayUrl: string, ok: boolean, error?: string): void {
@@ -365,7 +358,7 @@ export class RelayPublishOpBatch {
     )
     const ok = this.results.filter((r) => r.ok)
     const fail = this.results.filter((r) => !r.ok)
-    const sorted = this.results.sort((a, b) => a.cmdIndex - b.cmdIndex)
+    this.results.sort((a, b) => a.cmdIndex - b.cmdIndex)
     const readableSummary =
       this.relays.length === 0
         ? 'No relays targeted (empty list or skipped by session rules).'
@@ -381,31 +374,25 @@ export class RelayPublishOpBatch {
           ]
             .filter(Boolean)
             .join('\n')
-    logger.debug('[RelayOp] publish_batch_end', {
-      batchId: this.batchId,
-      source: this.source,
-      eventId: this.eventId,
-      status,
-      elapsedMs,
-      okCount: ok.length,
-      failCount: fail.length,
-      readableSummary,
-      byState: {
-        ok: {
-          count: ok.length,
-          relays: ok.map((r) => r.relayUrl),
-          hosts: ok.map((r) => relayHostForPublishLog(r.relayUrl)),
-          cmdIndices: ok.map((r) => r.cmdIndex)
-        },
-        fail: {
-          count: fail.length,
-          relays: fail.map((r) => r.relayUrl),
-          hosts: fail.map((r) => relayHostForPublishLog(r.relayUrl)),
-          cmdIndices: fail.map((r) => r.cmdIndex),
-          errors: fail.map((r) => r.error ?? '')
-        }
-      },
-      results: sorted
-    })
+    if (this.relays.length === 0 && status === 'no_targets') {
+      logger.warn('[RelayOp] publish_batch_end — no relay targets', {
+        batchId: this.batchId,
+        source: this.source,
+        eventId: this.eventId,
+        status
+      })
+      return
+    }
+    if (fail.length > 0) {
+      logger.warn(`[RelayOp] publish_batch_end — ${readableSummary}`, {
+        batchId: this.batchId,
+        source: this.source,
+        eventId: this.eventId,
+        status,
+        elapsedMs,
+        okCount: ok.length,
+        failCount: fail.length
+      })
+    }
   }
 }