Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
92fe1c262d
  1. 238
      src/components/SearchResult/FullTextSearchByRelay.tsx
  2. 4
      src/constants.ts
  3. 11
      src/i18n/locales/en.ts
  4. 31
      src/services/client-query.service.ts
  5. 9
      src/services/client.service.ts

238
src/components/SearchResult/FullTextSearchByRelay.tsx

@ -1,10 +1,9 @@
import NoteCard from '@/components/NoteCard' import NoteCard from '@/components/NoteCard'
import { Badge } from '@/components/ui/badge' import RelayIcon from '@/components/RelayIcon'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader } from '@/components/ui/card'
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 logger from '@/lib/logger'
import { cn } from '@/lib/utils'
import { normalizeUrl } from '@/lib/url' import { normalizeUrl } from '@/lib/url'
import client from '@/services/client.service' import client from '@/services/client.service'
import { relayHostForSubscribeLog } from '@/services/relay-operation-log.service' import { relayHostForSubscribeLog } from '@/services/relay-operation-log.service'
@ -13,31 +12,48 @@ import { Loader2 } from 'lucide-react'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
/** One-shot NIP-50 REQ per relay; bounded wait so the page always reaches a terminal state. */ /** One-shot NIP-50 REQ per relay; bounded wait so the page always reaches a terminal state (see QueryService NIP-50 global floor). */
const FULL_TEXT_SEARCH_PER_RELAY_TIMEOUT_MS = 10_000 const FULL_TEXT_SEARCH_PER_RELAY_TIMEOUT_MS = 45_000
/** Avoid opening every index relay at once (pool + main thread); still completes all relays. */ /** Avoid opening every index relay at once (pool + main thread); still completes all relays. */
const FULL_TEXT_SEARCH_RELAY_CONCURRENCY = 3 const FULL_TEXT_SEARCH_RELAY_CONCURRENCY = 3
const FULL_TEXT_SEARCH_PER_RELAY_LIMIT = 80 const FULL_TEXT_SEARCH_PER_RELAY_LIMIT = 80
/** Cap rows per card so a hot relay cannot mount hundreds of {@link NoteCard}s at once. */ /** Per-relay cap before merge (limits duplicate work). */
const FULL_TEXT_SEARCH_MAX_NOTES_PER_RELAY = 40 const FULL_TEXT_SEARCH_MAX_NOTES_PER_RELAY = 40
/** Max merged unique notes shown after deduping across relays. */
const FULL_TEXT_SEARCH_MAX_MERGED_EVENTS = 150
type RelayCardPhase = 'loading' | 'done' | 'error' type RelayFetchPhase = 'loading' | 'done' | 'error'
type RelayCardModel = { type RelayFetchRow = {
relayUrl: string relayUrl: string
host: string host: string
phase: RelayCardPhase phase: RelayFetchPhase
events: Event[] eventCount?: number
ms?: number ms?: number
errorMessage?: string errorMessage?: string
} }
type MergedHit = {
event: Event
relayUrls: string[]
}
function normalizeRelayList(urls: readonly string[]): string[] { function normalizeRelayList(urls: readonly string[]): string[] {
return Array.from( return Array.from(
new Set(urls.map((u) => normalizeUrl(u) || u.trim()).filter((u): u is string => u.length > 0)) new Set(urls.map((u) => normalizeUrl(u) || u.trim()).filter((u): u is string => u.length > 0))
).sort((a, b) => relayHostForSubscribeLog(a).localeCompare(relayHostForSubscribeLog(b))) ).sort((a, b) => relayHostForSubscribeLog(a).localeCompare(relayHostForSubscribeLog(b)))
} }
function relayKey(url: string): string {
return (normalizeUrl(url) || url.trim()).toLowerCase()
}
function sortRelaysByHost(urls: readonly string[]): string[] {
return Array.from(new Set(urls.map((u) => normalizeUrl(u) || u.trim()).filter(Boolean) as string[])).sort((a, b) =>
relayHostForSubscribeLog(a).localeCompare(relayHostForSubscribeLog(b))
)
}
/** Console hint: what this one-shot outcome suggests about NIP-50 (never proof without NIP-11). */ /** Console hint: what this one-shot outcome suggests about NIP-50 (never proof without NIP-11). */
function nip50OutcomeHint(args: { function nip50OutcomeHint(args: {
phase: 'done' | 'error' phase: 'done' | 'error'
@ -67,21 +83,28 @@ export default function FullTextSearchByRelay({
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const runGeneration = useRef(0) const runGeneration = useRef(0)
const [cards, setCards] = useState<RelayCardModel[]>([]) const [relayRows, setRelayRows] = useState<RelayFetchRow[]>([])
const [mergedHits, setMergedHits] = useState<MergedHit[]>([])
const normalizedRelays = useMemo(() => normalizeRelayList(relayUrls), [relayUrls]) const normalizedRelays = useMemo(() => normalizeRelayList(relayUrls), [relayUrls])
const q = searchQuery.trim() const q = searchQuery.trim()
const timeoutSec = Math.round(FULL_TEXT_SEARCH_PER_RELAY_TIMEOUT_MS / 1000) const timeoutSec = Math.round(FULL_TEXT_SEARCH_PER_RELAY_TIMEOUT_MS / 1000)
const doneRelayCount = relayRows.filter((r) => r.phase === 'done' || r.phase === 'error').length
const errorRelayCount = relayRows.filter((r) => r.phase === 'error').length
const anyLoading = relayRows.some((r) => r.phase === 'loading')
const allTerminal =
relayRows.length > 0 && relayRows.every((r) => r.phase === 'done' || r.phase === 'error')
useEffect(() => { useEffect(() => {
const myRun = ++runGeneration.current const myRun = ++runGeneration.current
if (!q || normalizedRelays.length === 0) { if (!q || normalizedRelays.length === 0) {
setCards([]) setRelayRows([])
setMergedHits([])
return return
} }
/** React 18 Strict Mode (dev) mounts twice; bump invalidates the previous run’s workers and ignores stale fetches. */
const cleanupInvalidatePreviousRun = () => { const cleanupInvalidatePreviousRun = () => {
runGeneration.current += 1 runGeneration.current += 1
} }
@ -94,14 +117,14 @@ export default function FullTextSearchByRelay({
const poolSize = Math.min(FULL_TEXT_SEARCH_RELAY_CONCURRENCY, normalizedRelays.length) const poolSize = Math.min(FULL_TEXT_SEARCH_RELAY_CONCURRENCY, normalizedRelays.length)
setCards( setRelayRows(
normalizedRelays.map((relayUrl) => ({ normalizedRelays.map((relayUrl) => ({
relayUrl, relayUrl,
host: relayHostForSubscribeLog(relayUrl), host: relayHostForSubscribeLog(relayUrl),
phase: 'loading', phase: 'loading'
events: []
})) }))
) )
setMergedHits([])
let relayCursor = 0 let relayCursor = 0
const nextRelayUrl = (): string | undefined => { const nextRelayUrl = (): string | undefined => {
@ -109,6 +132,35 @@ export default function FullTextSearchByRelay({
return normalizedRelays[relayCursor++]! return normalizedRelays[relayCursor++]!
} }
const mergeIntoHits = (relayUrl: string, events: Event[]) => {
const rk = relayKey(relayUrl)
setMergedHits((prev) => {
const map = new Map<string, { event: Event; relays: Set<string> }>()
for (const hit of prev) {
map.set(hit.event.id, { event: hit.event, relays: new Set(hit.relayUrls.map((u) => relayKey(u))) })
}
for (const ev of events) {
const cur = map.get(ev.id)
if (cur) {
cur.relays.add(rk)
} else {
map.set(ev.id, { event: ev, relays: new Set([rk]) })
}
}
const urlByKey = new Map<string, string>()
for (const u of normalizedRelays) {
urlByKey.set(relayKey(u), normalizeUrl(u) || u)
}
return [...map.values()]
.map(({ event, relays }) => ({
event,
relayUrls: sortRelaysByHost([...relays].map((k) => urlByKey.get(k) || k))
}))
.sort((a, b) => compareEventsForDTagQuery(q, a.event, b.event))
.slice(0, FULL_TEXT_SEARCH_MAX_MERGED_EVENTS)
})
}
const runOneRelay = async (relayUrl: string) => { const runOneRelay = async (relayUrl: string) => {
const host = relayHostForSubscribeLog(relayUrl) const host = relayHostForSubscribeLog(relayUrl)
logger.debug('[NIP-50 full-text] card_begin', { logger.debug('[NIP-50 full-text] card_begin', {
@ -128,7 +180,9 @@ export default function FullTextSearchByRelay({
) )
if (myRun !== runGeneration.current) return if (myRun !== runGeneration.current) return
const sorted = [...raw].sort((a, b) => compareEventsForDTagQuery(q, a, b)).slice(0, FULL_TEXT_SEARCH_MAX_NOTES_PER_RELAY) const sorted = [...raw]
.sort((a, b) => compareEventsForDTagQuery(q, a, b))
.slice(0, FULL_TEXT_SEARCH_MAX_NOTES_PER_RELAY)
for (const e of sorted) { for (const e of sorted) {
client.addEventToCache(e, { explicitNoteLookupHexId: e.id }) client.addEventToCache(e, { explicitNoteLookupHexId: e.id })
} }
@ -147,22 +201,18 @@ export default function FullTextSearchByRelay({
cardErrorMessage: connectionError, cardErrorMessage: connectionError,
nip50Hint: nip50OutcomeHint({ phase: 'error', rawCount: 0, connectionError }) nip50Hint: nip50OutcomeHint({ phase: 'error', rawCount: 0, connectionError })
}) })
setCards((prev) => setRelayRows((prev) =>
prev.map((c) => prev.map((r) =>
c.relayUrl === relayUrl r.relayUrl === relayUrl
? { ? { ...r, phase: 'error', eventCount: 0, ms, errorMessage: connectionError }
...c, : r
phase: 'error',
events: [],
ms,
errorMessage: connectionError
}
: c
) )
) )
return return
} }
mergeIntoHits(relayUrl, sorted)
logger.debug('[NIP-50 full-text] card_end', { logger.debug('[NIP-50 full-text] card_end', {
runId: myRun, runId: myRun,
relayUrl, relayUrl,
@ -185,17 +235,17 @@ export default function FullTextSearchByRelay({
}) })
}) })
setCards((prev) => setRelayRows((prev) =>
prev.map((c) => prev.map((r) =>
c.relayUrl === relayUrl r.relayUrl === relayUrl
? { ? {
...c, ...r,
phase: 'done', phase: 'done',
events: sorted, eventCount: sorted.length,
ms, ms,
errorMessage: sorted.length > 0 ? undefined : connectionError errorMessage: sorted.length > 0 ? undefined : connectionError
} }
: c : r
) )
) )
} catch (err) { } catch (err) {
@ -214,17 +264,9 @@ export default function FullTextSearchByRelay({
cardErrorMessage: msg, cardErrorMessage: msg,
nip50Hint: nip50OutcomeHint({ phase: 'error', rawCount: 0 }) nip50Hint: nip50OutcomeHint({ phase: 'error', rawCount: 0 })
}) })
setCards((prev) => setRelayRows((prev) =>
prev.map((c) => prev.map((r) =>
c.relayUrl === relayUrl r.relayUrl === relayUrl ? { ...r, phase: 'error', eventCount: 0, ms, errorMessage: msg } : r
? {
...c,
phase: 'error',
events: [],
ms,
errorMessage: msg
}
: c
) )
) )
} }
@ -250,23 +292,19 @@ export default function FullTextSearchByRelay({
try { try {
await Promise.all(Array.from({ length: poolSize }, () => worker())) await Promise.all(Array.from({ length: poolSize }, () => worker()))
} catch { } catch {
/* runOneRelay already updates card errors */ /* runOneRelay already updates relay rows */
} }
if (myRun !== runGeneration.current) return if (myRun !== runGeneration.current) return
logger.debug('[NIP-50 full-text] wave_end', { logger.debug('[NIP-50 full-text] wave_end', {
runId: myRun, runId: myRun,
relayCount: normalizedRelays.length, relayCount: normalizedRelays.length,
note: 'matches UI "all relays finished" when every card is done or error' note: 'matches UI "all relays finished" when every relay row is done or error'
}) })
})() })()
return cleanupInvalidatePreviousRun return cleanupInvalidatePreviousRun
}, [q, normalizedRelays, kinds]) }, [q, normalizedRelays, kinds])
const allTerminal =
cards.length > 0 && cards.every((c) => c.phase === 'done' || c.phase === 'error')
const anyLoading = cards.some((c) => c.phase === 'loading')
if (!q) { if (!q) {
return null return null
} }
@ -274,76 +312,70 @@ export default function FullTextSearchByRelay({
return ( return (
<div className="min-w-0 space-y-4" aria-busy={anyLoading}> <div className="min-w-0 space-y-4" aria-busy={anyLoading}>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{t('Full-text search per relay intro', { {t('Full-text search merged intro', {
relayCount: normalizedRelays.length, relayCount: normalizedRelays.length,
seconds: timeoutSec, seconds: timeoutSec,
concurrency: FULL_TEXT_SEARCH_RELAY_CONCURRENCY concurrency: FULL_TEXT_SEARCH_RELAY_CONCURRENCY
})} })}
</p> </p>
<div {relayRows.length > 0 && (
className={cn( <p className="text-xs text-muted-foreground flex items-center gap-2" role="status">
'grid gap-4 min-w-0', {anyLoading && <Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin" aria-hidden />}
'grid-cols-1 md:grid-cols-2 xl:grid-cols-3' {t('Full-text search progress relays', { done: doneRelayCount, total: relayRows.length })}
)}
>
{cards.map((c) => (
<Card key={c.relayUrl} className="min-w-0 flex flex-col overflow-hidden">
<CardHeader className="pb-2 space-y-1">
<div className="flex items-start justify-between gap-2">
<CardTitle className="text-base font-medium break-all">{c.host}</CardTitle>
{c.phase === 'loading' ? (
<Loader2 className="h-4 w-4 shrink-0 animate-spin text-muted-foreground" aria-hidden />
) : (
<Badge variant="secondary" className="shrink-0">
{c.events.length}
</Badge>
)}
</div>
<CardDescription className="break-all text-xs font-mono opacity-80">{c.relayUrl}</CardDescription>
{c.phase === 'done' && c.ms != null && (
<p className="text-xs text-muted-foreground">
{t('Full-text search relay timing', { ms: c.ms })}
</p> </p>
)} )}
</CardHeader>
<CardContent className="flex-1 min-h-0 pt-0 flex flex-col gap-2"> {errorRelayCount > 0 && allTerminal && (
{c.phase === 'loading' && ( <p className="text-xs text-amber-600 dark:text-amber-500" role="status">
<div className="space-y-2" aria-label={t('Full-text search relay querying')}> {t('Full-text search relay errors summary', { count: errorRelayCount })}
<Skeleton className="h-16 w-full" />
<Skeleton className="h-16 w-full" />
<Skeleton className="h-12 w-full" />
</div>
)}
{c.phase === 'error' && (
<p className="text-sm text-destructive">
{t('Full-text search relay error')}: {c.errorMessage ?? t('Full-text search relay unknown error')}
</p> </p>
)} )}
{c.phase === 'done' && c.events.length === 0 && !c.errorMessage && (
<p className="text-sm text-muted-foreground">{t('Full-text search relay no hits')}</p> <div className="min-w-0 space-y-4">
)} {anyLoading && mergedHits.length === 0 && (
{c.phase === 'done' && c.events.length === 0 && c.errorMessage && ( <div className="space-y-3" aria-label={t('Full-text search relay querying')}>
<p className="text-sm text-muted-foreground">{c.errorMessage}</p> <Skeleton className="h-24 w-full" />
<Skeleton className="h-24 w-full" />
<Skeleton className="h-20 w-full" />
</div>
)} )}
{c.events.length > 0 && (
<ul {mergedHits.map((hit) => (
className="max-h-[min(28rem,55vh)] overflow-y-auto space-y-3 pr-1 -mr-1 min-w-0" <Card key={hit.event.id} className="min-w-0 overflow-hidden">
role="list" <CardHeader className="pb-2 space-y-2 border-b bg-muted/30">
<div
className="flex flex-wrap items-center gap-1.5"
aria-label={t('Full-text search seen on relays')}
>
<span className="text-xs text-muted-foreground shrink-0 mr-1">
{t('Full-text search seen on label')}
</span>
{hit.relayUrls.map((url) => (
<span
key={`${hit.event.id}-${relayKey(url)}`}
title={relayHostForSubscribeLog(url)}
className="inline-flex shrink-0"
> >
{c.events.map((ev) => ( <RelayIcon url={url} skipRelayInfoFetch className="h-7 w-7 rounded-sm" iconSize={14} />
<li key={ev.id} className="min-w-0"> </span>
<NoteCard event={ev} className="w-full" filterMutedNotes />
</li>
))} ))}
</ul> </div>
)} </CardHeader>
<CardContent className="pt-4">
<NoteCard event={hit.event} className="w-full border-0 shadow-none p-0" filterMutedNotes />
</CardContent> </CardContent>
</Card> </Card>
))} ))}
</div> </div>
{allTerminal && ( {allTerminal && mergedHits.length === 0 && (
<p className="text-sm text-muted-foreground" role="status">
{t('Full-text search empty merged')}
</p>
)}
{allTerminal && mergedHits.length > 0 && (
<p className="text-sm text-muted-foreground border-t pt-3" role="status"> <p className="text-sm text-muted-foreground border-t pt-3" role="status">
{t('Full-text search all relays finished')} {t('Full-text search all relays finished')}
</p> </p>

4
src/constants.ts

@ -435,10 +435,6 @@ export const SEARCHABLE_RELAY_URLS = [
'wss://search.nos.today', 'wss://search.nos.today',
'wss://nostr.wine', 'wss://nostr.wine',
'wss://orly-relay.imwald.eu', 'wss://orly-relay.imwald.eu',
'wss://aggr.nostr.land',
'wss://thecitadel.nostr1.com',
'wss://nos.lol',
'wss://nostr.mom',
'wss://relay.noswhere.com', 'wss://relay.noswhere.com',
'wss://relay.wikifreedia.xyz', 'wss://relay.wikifreedia.xyz',
'wss://nostr.einundzwanzig.space', 'wss://nostr.einundzwanzig.space',

11
src/i18n/locales/en.ts

@ -1851,11 +1851,14 @@ export default {
"Search threads by title, content, tags, npub, author...": "Search threads by title, content, tags, npub, author...", "Search threads by title, content, tags, npub, author...": "Search threads by title, content, tags, npub, author...",
"Searching all available relays...": "Searching all available relays...", "Searching all available relays...": "Searching all available relays...",
"Searching…": "Searching…", "Searching…": "Searching…",
"Full-text search per relay intro": "Full-text search merged intro":
"Each card runs one bounded NIP-50 query on that index relay ({{relayCount}} relays, {{seconds}}s timeout each, up to {{concurrency}} in parallel so the tab stays responsive). This is not a live feed — results do not auto-update.", "Results are merged by note: each card shows one event and which index relays returned it ({{relayCount}} relays, up to {{seconds}}s per relay, up to {{concurrency}} in parallel). This is not a live feed — results do not auto-update.",
"Full-text search progress relays": "{{done}} / {{total}} index relays",
"Full-text search seen on label": "Seen on",
"Full-text search seen on relays": "Relays that returned this note",
"Full-text search empty merged": "No notes matched this search on any index relay.",
"Full-text search relay errors summary": "{{count}} relay(s) could not be queried.",
"Full-text search relay querying": "Querying relay…", "Full-text search relay querying": "Querying relay…",
"Full-text search relay timing": "Finished in {{ms}} ms",
"Full-text search relay no hits": "No hits on this relay.",
"Full-text search relay error": "Query failed", "Full-text search relay error": "Query failed",
"Full-text search relay unknown error": "Unknown error", "Full-text search relay unknown error": "Unknown error",
"Full-text search all relays finished": "All relay queries have finished.", "Full-text search all relays finished": "All relay queries have finished.",

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

@ -51,6 +51,18 @@ function filterForRelay(f: Filter, relaySupportsSearch: boolean): Filter {
return rest as Filter return rest as Filter
} }
function filtersHaveNip50Search(filters: readonly Filter[]): boolean {
return filters.some((f) => typeof f.search === 'string' && f.search.trim().length > 0)
}
/** NIP-50 index relays answer after connect + slot wait; nostr-tools synthetic EOSE runs this long after REQ `fire()`. */
const NIP50_RELAY_SUBSCRIPTION_EOSE_TIMEOUT_MS = 38_000
/**
* {@link QueryService.query} `globalTimeout` is armed at call start; REQ may start seconds later. Used only for
* {@link ClientService.fetchEventsFromSingleRelay} so mention/picker queries keep their own shorter caps.
*/
const NIP50_QUERY_GLOBAL_TIMEOUT_FLOOR_MS = 42_000
const HEX_EVENT_ID_RE = /^[0-9a-f]{64}$/i const HEX_EVENT_ID_RE = /^[0-9a-f]{64}$/i
let queryReqSeq = 0 let queryReqSeq = 0
@ -384,7 +396,13 @@ export class QueryService {
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 eoseTimeout = options?.eoseTimeout ?? 500
const globalTimeout = options?.globalTimeout ?? 10000 const hasNip50Search = filtersHaveNip50Search(sanitizedFilters)
const globalTimeoutRaw = options?.globalTimeout ?? 10000
const useNip50QueryTimeoutFloor =
hasNip50Search && options?.relayOpSource === 'fetchEventsFromSingleRelay'
const globalTimeout = useNip50QueryTimeoutFloor
? Math.max(globalTimeoutRaw, NIP50_QUERY_GLOBAL_TIMEOUT_FLOOR_MS)
: globalTimeoutRaw
const replaceableRace = options?.replaceableRace ?? false const replaceableRace = options?.replaceableRace ?? false
const replaceableRaceWaitMs = options?.replaceableRaceWaitMs ?? FIRST_RELAY_RESULT_GRACE_MS const replaceableRaceWaitMs = options?.replaceableRaceWaitMs ?? FIRST_RELAY_RESULT_GRACE_MS
const immediateReturn = options?.immediateReturn ?? false const immediateReturn = options?.immediateReturn ?? false
@ -746,9 +764,10 @@ export class QueryService {
return { url, filters: filtersForRelay } return { url, filters: filtersForRelay }
}) })
const hasNip50Search = filters.some( const hasNip50Search = filtersHaveNip50Search(filters)
(f) => typeof f.search === 'string' && f.search.trim().length > 0 const relaySubscriptionEoseTimeoutMs = hasNip50Search
) ? NIP50_RELAY_SUBSCRIPTION_EOSE_TIMEOUT_MS
: 10_000
/** /**
* Single-relay `pool.close` before subscribe resets the socket. Overlapping NIP-50 one-shots (e.g. Strict Mode * Single-relay `pool.close` before subscribe resets the socket. Overlapping NIP-50 one-shots (e.g. Strict Mode
* double effect) then tear down each others REQ before EOSE empty results until globalTimeout. * double effect) then tear down each others REQ before EOSE empty results until globalTimeout.
@ -896,7 +915,7 @@ export class QueryService {
handleClose(i, reason2) handleClose(i, reason2)
}, },
alreadyHaveEvent: localAlreadyHaveEvent, alreadyHaveEvent: localAlreadyHaveEvent,
eoseTimeout: 10_000 eoseTimeout: relaySubscriptionEoseTimeoutMs
}) })
subs.push({ subs.push({
relayKey, relayKey,
@ -928,7 +947,7 @@ export class QueryService {
handleClose(i, reason) handleClose(i, reason)
}, },
alreadyHaveEvent: localAlreadyHaveEvent, alreadyHaveEvent: localAlreadyHaveEvent,
eoseTimeout: 10_000 eoseTimeout: relaySubscriptionEoseTimeoutMs
}) })
subs.push({ subs.push({
relayKey, relayKey,

9
src/services/client.service.ts

@ -2393,7 +2393,14 @@ class ClientService extends EventTarget {
return { url, filters: filtersForRelay } return { url, filters: filtersForRelay }
}) })
if (groupedRequests.length === 1) { const hasNip50Search = filters.some(
(f) => typeof f.search === 'string' && f.search.trim().length > 0
)
/**
* Same rule as {@link QueryService.subscribe}: never `pool.close` a lone relay when the REQ carries NIP-50
* `search` overlapping one-shots (e.g. Strict Mode) otherwise reset the socket before EOSE.
*/
if (groupedRequests.length === 1 && !hasNip50Search) {
try { try {
this.pool.close([groupedRequests[0]!.url]) this.pool.close([groupedRequests[0]!.url])
} catch { } catch {

Loading…
Cancel
Save