Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
c3cbed749c
  1. 14
      src/components/Note/index.tsx
  2. 5
      src/lib/nostr-relay-auth-patch.ts
  3. 27
      src/lib/relay-strikes.ts
  4. 37
      src/services/client-query.service.ts
  5. 13
      src/services/client.service.ts
  6. 63
      src/services/relay-operation-log.service.ts

14
src/components/Note/index.tsx

@ -318,6 +318,9 @@ export default function Note({
hideMetadata?: boolean hideMetadata?: boolean
className?: string className?: string
} = {}) => { } = {}) => {
if (isNip18RepostKind(displayEvent.kind)) {
return <RepostEventContent className={className} event={displayEvent} />
}
const embeddedEvent = findTrailingStringifiedNostrEvent(displayEvent.content ?? '') const embeddedEvent = findTrailingStringifiedNostrEvent(displayEvent.content ?? '')
if (embeddedEvent) { if (embeddedEvent) {
return ( return (
@ -333,6 +336,9 @@ export default function Note({
) )
} }
if (isStringifiedJsonContent(displayEvent.content)) { if (isStringifiedJsonContent(displayEvent.content)) {
if (isNip18RepostKind(displayEvent.kind)) {
return <RepostEventContent className={className} event={displayEvent} />
}
return ( return (
<pre <pre
className={cn( className={cn(
@ -379,7 +385,11 @@ export default function Note({
return ( return (
<MarkdownArticle <MarkdownArticle
className={className} className={className}
event={displayEvent} event={
isNip18RepostKind(displayEvent.kind)
? { ...displayEvent, content: '' }
: displayEvent
}
hideMetadata={hideMetadata} hideMetadata={hideMetadata}
lazyMedia={!autoLoadMedia} lazyMedia={!autoLoadMedia}
fullCalendarInvite={fullCalendarInvite} fullCalendarInvite={fullCalendarInvite}
@ -399,7 +409,7 @@ export default function Note({
content = <NsfwNote show={() => setShowNsfw(true)} /> content = <NsfwNote show={() => setShowNsfw(true)} />
} else if (isNip25ReactionKind(event.kind)) { } else if (isNip25ReactionKind(event.kind)) {
content = null content = null
} else if (isNip18RepostKind(event.kind)) { } else if (isNip18RepostKind(displayEvent.kind)) {
content = <RepostEventContent className="mt-2" event={displayEvent} /> content = <RepostEventContent className="mt-2" event={displayEvent} />
} else if (event.kind === ExtendedKind.POLL_RESPONSE) { } else if (event.kind === ExtendedKind.POLL_RESPONSE) {
content = <NotificationEventCard className="mt-2" event={displayEvent} /> content = <NotificationEventCard className="mt-2" event={displayEvent} />

5
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 { notifyRelayNip42Accepted, notifyRelayNip42Rejected } from '@/lib/relay-auth-feedback'
import type { AbstractRelay } from 'nostr-tools/abstract-relay' import type { AbstractRelay } from 'nostr-tools/abstract-relay'
import type { EventTemplate, VerifiedEvent } from 'nostr-tools' import type { EventTemplate, VerifiedEvent } from 'nostr-tools'
@ -65,9 +64,6 @@ export function patchPoolRelayAuthRaceAndFeedback(relay: object): void {
const r = asRelayInternals(this) const r = asRelayInternals(this)
if (!r.connectionPromise && typeof message === 'string' && message.startsWith('["AUTH"')) { if (!r.connectionPromise && typeof message === 'string' && message.startsWith('["AUTH"')) {
abortPendingAuthForDeadSocket(r, message) abortPendingAuthForDeadSocket(r, message)
logger.debug('[RelayOp] Dropped AUTH (socket already closed; connect timeout vs signing race)', {
url: r.url
})
return Promise.resolve() return Promise.resolve()
} }
return origSend.call(this, message) as Promise<void> return origSend.call(this, message) as Promise<void>
@ -91,7 +87,6 @@ export function patchPoolRelayAuthRaceAndFeedback(relay: object): void {
msg.includes('relay connection closed before AUTH') || msg.includes('relay connection closed before AUTH') ||
/relay connection closed/i.test(msg) /relay connection closed/i.test(msg)
if (benignRace) { if (benignRace) {
logger.debug('[RelayOp] Relay AUTH aborted (benign race)', { url: r.url, detail: msg })
r.authPromise = undefined r.authPromise = undefined
return '' return ''
} }

27
src/lib/relay-strikes.ts

@ -65,8 +65,6 @@ function sessionKey(url: string): string {
class RelaySessionStrikes { class RelaySessionStrikes {
private byKey = new Map<string, StrikeEntry>() private byKey = new Map<string, StrikeEntry>()
private cacheRelayKeys = new Set<string>() private cacheRelayKeys = new Set<string>()
/** Throttle debug spam when many parallel REQs hit the same dead relay (cache rows bypass strike debounce). */
private lastReadFailureDebugLogAt = new Map<string, number>()
setSessionCacheRelayKeysFromKind10432(ev: Event | null | undefined): void { setSessionCacheRelayKeysFromKind10432(ev: Event | null | undefined): void {
this.cacheRelayKeys.clear() this.cacheRelayKeys.clear()
@ -136,13 +134,8 @@ class RelaySessionStrikes {
} }
private applyRateLimitCooldownKey(key: string): void { private applyRateLimitCooldownKey(key: string): void {
const now = Date.now()
const e = this.getEntry(key) const e = this.getEntry(key)
e.rateLimitUntil = Math.max(e.rateLimitUntil, now + RATE_LIMIT_COOLDOWN_MS) e.rateLimitUntil = Math.max(e.rateLimitUntil, Date.now() + RATE_LIMIT_COOLDOWN_MS)
logger.debug('[RelayStrikes] rate-limit cooldown', {
key,
untilMs: e.rateLimitUntil - now
})
} }
/** WS connect failure, HTTP transport failure, etc. */ /** 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. // Cache relays from kind 10432 (e.g. localhost on another machine) always accrue failures.
if (now < e.rateLimitUntil && !this.cacheRelayKeys.has(key)) return if (now < e.rateLimitUntil && !this.cacheRelayKeys.has(key)) return
const cache = this.cacheRelayKeys.has(key) if (!this.cacheRelayKeys.has(key)) {
if (!cache) {
if (now - e.readLastStrikeIncrementAt < STRIKE_INCREMENT_DEBOUNCE_MS) return if (now - e.readLastStrikeIncrementAt < STRIKE_INCREMENT_DEBOUNCE_MS) return
e.readLastStrikeIncrementAt = now e.readLastStrikeIncrementAt = now
} }
@ -168,13 +160,7 @@ class RelaySessionStrikes {
e.readFailures += 1 e.readFailures += 1
if (e.readFailures >= STRIKE_FAILURES_THRESHOLD) { if (e.readFailures >= STRIKE_FAILURES_THRESHOLD) {
e.readStrikeSkipUntil = Math.max(e.readStrikeSkipUntil, now + STRIKE_COOLDOWN_MS) e.readStrikeSkipUntil = Math.max(e.readStrikeSkipUntil, now + STRIKE_COOLDOWN_MS)
logger.info('[RelayStrikes] read path strike skip', { key, readFailures: e.readFailures }) logger.warn('[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 })
}
} }
} }
@ -186,7 +172,6 @@ class RelaySessionStrikes {
e.readFailures = 0 e.readFailures = 0
e.readStrikeSkipUntil = 0 e.readStrikeSkipUntil = 0
e.readLastStrikeIncrementAt = 0 e.readLastStrikeIncrementAt = 0
this.lastReadFailureDebugLogAt.delete(key)
} }
recordPublishFailure(url: string): void { recordPublishFailure(url: string): void {
@ -195,15 +180,14 @@ class RelaySessionStrikes {
const now = Date.now() const now = Date.now()
const e = this.getEntry(key) const e = this.getEntry(key)
if (now < e.rateLimitUntil && !this.cacheRelayKeys.has(key)) return if (now < e.rateLimitUntil && !this.cacheRelayKeys.has(key)) return
const cache = this.cacheRelayKeys.has(key) if (!this.cacheRelayKeys.has(key)) {
if (!cache) {
if (now - e.publishLastStrikeIncrementAt < STRIKE_INCREMENT_DEBOUNCE_MS) return if (now - e.publishLastStrikeIncrementAt < STRIKE_INCREMENT_DEBOUNCE_MS) return
e.publishLastStrikeIncrementAt = now e.publishLastStrikeIncrementAt = now
} }
e.publishFailures += 1 e.publishFailures += 1
if (e.publishFailures >= STRIKE_FAILURES_THRESHOLD) { if (e.publishFailures >= STRIKE_FAILURES_THRESHOLD) {
e.publishStrikeSkipUntil = Math.max(e.publishStrikeSkipUntil, now + STRIKE_COOLDOWN_MS) 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 { reset(): void {
this.byKey.clear() this.byKey.clear()
this.cacheRelayKeys.clear() this.cacheRelayKeys.clear()
this.lastReadFailureDebugLogAt.clear()
} }
} }

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

@ -28,7 +28,7 @@ import {
import { applyRelayNip42AckTimeout } from '@/lib/relay-nip42-tuning' 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 { canonicalRelaySessionKey, isHttpRelayUrl, normalizeHttpRelayUrl, normalizeUrl } from '@/lib/url'
import { RelaySubscribeOpBatch, 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 { 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'
@ -405,11 +405,14 @@ export class QueryService {
/** One chunk → pass a single Filter (compat); several (e.g. kinds split) → full array for WS + HTTP. */ /** One chunk → pass a single Filter (compat); several (e.g. kinds split) → full array for WS + HTTP. */
const effectiveFilter: Filter | Filter[] = const effectiveFilter: Filter | Filter[] =
sanitizedFilters.length === 1 ? sanitizedFilters[0]! : sanitizedFilters sanitizedFilters.length === 1 ? sanitizedFilters[0]! : sanitizedFilters
const eoseTimeout = options?.eoseTimeout ?? 500
const hasNip50Search = filtersHaveNip50Search(sanitizedFilters) const hasNip50Search = filtersHaveNip50Search(sanitizedFilters)
const globalTimeoutRaw = options?.globalTimeout ?? 10000
const useNip50QueryTimeoutFloor = const useNip50QueryTimeoutFloor =
hasNip50Search && options?.relayOpSource === 'fetchEventsFromSingleRelay' 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 const globalTimeout = useNip50QueryTimeoutFloor
? Math.max(globalTimeoutRaw, NIP50_QUERY_GLOBAL_TIMEOUT_FLOOR_MS) ? Math.max(globalTimeoutRaw, NIP50_QUERY_GLOBAL_TIMEOUT_FLOOR_MS)
: globalTimeoutRaw : globalTimeoutRaw
@ -733,7 +736,7 @@ export class QueryService {
relayOpMeta?: { relayOpMeta?: {
source: string source: string
logLevel?: 'info' | 'debug' 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 quiet?: boolean
onBatchEnd?: (rows: RelayOpTerminalRow[]) => void onBatchEnd?: (rows: RelayOpTerminalRow[]) => void
} }
@ -780,19 +783,27 @@ export class QueryService {
grouped.get(key)!.push(...filters) grouped.get(key)!.push(...filters)
} }
const searchableSet = new Set([ const searchableSet = new Set(
...SEARCHABLE_RELAY_URLS.map((u) => normalizeUrl(u) || u), [
...nip66Service.getSearchableRelayUrls().map((u) => normalizeUrl(u) || u), ...SEARCHABLE_RELAY_URLS,
...PROFILE_RELAY_URLS.map((u) => normalizeUrl(u) || u).filter(Boolean) ...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 groupedRequests = Array.from(grouped.entries()).map(([url, f]) => {
const relaySupportsSearch = searchableSet.has(url) || nip66Service.isRelaySearchable(url) const sessionKey = canonicalRelaySessionKey(url)
const filtersForRelay = f.map((one) => filterForRelay(one, relaySupportsSearch)) const relaySupportsSearch =
searchableSet.has(sessionKey) || nip66Service.isRelaySearchable(url)
const filtersForRelay = singleRelayNip50 ? f : f.map((one) => filterForRelay(one, relaySupportsSearch))
return { url, filters: filtersForRelay } return { url, filters: filtersForRelay }
}) })
const hasNip50Search = filtersHaveNip50Search(filters)
const relaySubscriptionEoseTimeoutMs = hasNip50Search const relaySubscriptionEoseTimeoutMs = hasNip50Search
? NIP50_RELAY_SUBSCRIPTION_EOSE_TIMEOUT_MS ? NIP50_RELAY_SUBSCRIPTION_EOSE_TIMEOUT_MS
: 10_000 : 10_000

13
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 timelineBatchId = `tl-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 9)}`
const timelineT0 = performance.now() 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', { logger.debug('[relay-req] timeline_batch_start', {
timelineBatchId, timelineBatchId,
subRequestCount: subRequests.length, subRequestCount: subRequests.length,
@ -3243,6 +3232,8 @@ class ClientService extends EventTarget {
globalTimeout: options?.globalTimeout ?? 25_000, globalTimeout: options?.globalTimeout ?? 25_000,
relayOpSource: 'fetchEventsFromSingleRelay' as const, relayOpSource: 'fetchEventsFromSingleRelay' as const,
foreground: true 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 } : {}) ...(options?.signal ? { signal: options.signal } : {})
} }

63
src/services/relay-operation-log.service.ts

@ -139,9 +139,9 @@ function groupTerminalsByOutcome(rows: RelayOpTerminalRow[]): Record<string, { c
* Tracks one logical subscribe/query wave: one `batch_begin` and one `batch_end` with per-relay outcomes. * Tracks one logical subscribe/query wave: one `batch_begin` and one `batch_end` with per-relay outcomes.
*/ */
export type RelaySubscribeOpBatchOptions = { export type RelaySubscribeOpBatchOptions = {
/** `info` logs every REQ wave at INFO; default `debug` keeps subscribe noise behind jumble-debug / VITE_DEBUG. */ /** When `quiet` is false: `info` logs batch_end at INFO; `debug` logs batch_begin/batch_end at DEBUG. */
logLevel?: 'info' | 'debug' logLevel?: 'info' | 'debug'
/** When true, skip `[RelayOp] batch_begin` / `batch_end` lines (e.g. when {@link QueryService.query} logs `req_begin`/`req_end`). */ /** When true (default), skip `[RelayOp] batch_begin` / `batch_end`. Set false to opt into REQ wave logs. */
quiet?: boolean quiet?: boolean
/** Invoked once when this REQ wave finishes (same `rows` as `batch_end` / `terminals`). */ /** Invoked once when this REQ wave finishes (same `rows` as `batch_end` / `terminals`). */
onBatchEnd?: (rows: RelayOpTerminalRow[]) => void onBatchEnd?: (rows: RelayOpTerminalRow[]) => void
@ -198,7 +198,7 @@ export class RelaySubscribeOpBatch {
this.source = source this.source = source
this.grouped = grouped this.grouped = grouped
this.logLevel = options?.logLevel ?? 'debug' this.logLevel = options?.logLevel ?? 'debug'
this.quiet = options?.quiet ?? false this.quiet = options?.quiet ?? true
this.onBatchEnd = options?.onBatchEnd this.onBatchEnd = options?.onBatchEnd
} }
@ -340,14 +340,7 @@ export class RelayPublishOpBatch {
} }
logBegin(): void { logBegin(): void {
logger.debug('[RelayOp] publish_batch_begin', { /* Intentionally quiet: publish outcomes surface in UI; failures are logged in logEnd. */
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 }))
})
} }
record(cmdIndex: number, relayUrl: string, ok: boolean, error?: string): void { 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 ok = this.results.filter((r) => r.ok)
const fail = 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 = const readableSummary =
this.relays.length === 0 this.relays.length === 0
? 'No relays targeted (empty list or skipped by session rules).' ? 'No relays targeted (empty list or skipped by session rules).'
@ -381,31 +374,25 @@ export class RelayPublishOpBatch {
] ]
.filter(Boolean) .filter(Boolean)
.join('\n') .join('\n')
logger.debug('[RelayOp] publish_batch_end', { if (this.relays.length === 0 && status === 'no_targets') {
batchId: this.batchId, logger.warn('[RelayOp] publish_batch_end — no relay targets', {
source: this.source, batchId: this.batchId,
eventId: this.eventId, source: this.source,
status, eventId: this.eventId,
elapsedMs, status
okCount: ok.length, })
failCount: fail.length, return
readableSummary, }
byState: { if (fail.length > 0) {
ok: { logger.warn(`[RelayOp] publish_batch_end — ${readableSummary}`, {
count: ok.length, batchId: this.batchId,
relays: ok.map((r) => r.relayUrl), source: this.source,
hosts: ok.map((r) => relayHostForPublishLog(r.relayUrl)), eventId: this.eventId,
cmdIndices: ok.map((r) => r.cmdIndex) status,
}, elapsedMs,
fail: { okCount: ok.length,
count: fail.length, failCount: 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
})
} }
} }

Loading…
Cancel
Save