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
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
+ })
+ }
}
}