Browse Source

bug-fix

imwald
Silberengel 1 month ago
parent
commit
f7b3ab4668
  1. 19
      src/components/SearchResult/FullTextSearchByRelay.tsx
  2. 12
      src/components/SearchResult/index.tsx
  3. 3
      src/providers/NostrProvider/index.tsx
  4. 57
      src/services/client-query.service.ts
  5. 2
      src/services/client-replaceable-events.service.ts
  6. 11
      src/services/client.service.ts

19
src/components/SearchResult/FullTextSearchByRelay.tsx

@ -242,15 +242,20 @@ export default function FullTextSearchByRelay({
relayRows.length > 0 && relayRows.every((r) => r.phase === 'done' || r.phase === 'error') relayRows.length > 0 && relayRows.every((r) => r.phase === 'done' || r.phase === 'error')
useEffect(() => { useEffect(() => {
const abort = new AbortController()
const myRun = ++runGeneration.current const myRun = ++runGeneration.current
const cleanupInvalidatePreviousRun = () => {
runGeneration.current += 1
}
const dispose = () => {
abort.abort()
cleanupInvalidatePreviousRun()
}
if (!q || normalizedRelays.length === 0) { if (!q || normalizedRelays.length === 0) {
setRelayRows([]) setRelayRows([])
setMergedHits([]) setMergedHits([])
return return dispose
}
const cleanupInvalidatePreviousRun = () => {
runGeneration.current += 1
} }
const filter: Filter = { const filter: Filter = {
@ -311,7 +316,7 @@ export default function FullTextSearchByRelay({
const { events: raw, connectionError } = await client.fetchEventsFromSingleRelay( const { events: raw, connectionError } = await client.fetchEventsFromSingleRelay(
relayUrl, relayUrl,
filter, filter,
{ globalTimeout: FULL_TEXT_SEARCH_PER_RELAY_TIMEOUT_MS } { globalTimeout: FULL_TEXT_SEARCH_PER_RELAY_TIMEOUT_MS, signal: abort.signal }
) )
if (myRun !== runGeneration.current) return if (myRun !== runGeneration.current) return
@ -375,7 +380,7 @@ export default function FullTextSearchByRelay({
} }
})() })()
return cleanupInvalidatePreviousRun return dispose
}, [q, normalizedRelays, kinds]) }, [q, normalizedRelays, kinds])
if (!q) { if (!q) {

12
src/components/SearchResult/index.tsx

@ -7,8 +7,9 @@ import { ProfileListBySearch } from '../ProfileListBySearch'
import Relay from '../Relay' import Relay from '../Relay'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import client from '@/services/client.service'
import { normalizeUrl } from '@/lib/url' import { normalizeUrl } from '@/lib/url'
import { useMemo } from 'react' import { useLayoutEffect, useMemo } from 'react'
function relayDedupeKey(url: string): string { function relayDedupeKey(url: string): string {
return (normalizeUrl(url) || url.trim()).toLowerCase() return (normalizeUrl(url) || url.trim()).toLowerCase()
@ -18,6 +19,13 @@ export default function SearchResult({ searchParams }: { searchParams: TSearchPa
const { pubkey, relayList } = useNostr() const { pubkey, relayList } = useNostr()
const { favoriteRelays, blockedRelays } = useFavoriteRelays() const { favoriteRelays, blockedRelays } = useFavoriteRelays()
/** Before child effects (e.g. NIP-50) open REQs, tear down idle feed / prefetch queries so search gets the pool. */
useLayoutEffect(() => {
if (!searchParams) return
if (searchParams.type === 'relay') return
client.interruptBackgroundQueries()
}, [searchParams?.type, searchParams?.search, searchParams?.input])
/** NIP-50 / index relays — always queried first on their own shard so dead personal relays cannot zero out search. */ /** NIP-50 / index relays — always queried first on their own shard so dead personal relays cannot zero out search. */
const searchableUrls = useMemo( const searchableUrls = useMemo(
() => () =>
@ -34,7 +42,7 @@ export default function SearchResult({ searchParams }: { searchParams: TSearchPa
// User stack + defaults (hashtag search uses the non-searchable slice as a second shard) // User stack + defaults (hashtag search uses the non-searchable slice as a second shard)
const combinedRelays = useMemo(() => { const combinedRelays = useMemo(() => {
let relays: string[] = [] const relays: string[] = []
if (relayList) { if (relayList) {
relays.push(...(relayList.read || []), ...(relayList.write || [])) relays.push(...(relayList.read || []), ...(relayList.write || []))

3
src/providers/NostrProvider/index.tsx

@ -1537,6 +1537,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
} }
} }
client.interruptBackgroundQueries()
noteStatsService.beginPublishPriority() noteStatsService.beginPublishPriority()
try { try {
logger.debug('[Publish] Determining target relays...', { kind: event.kind, pubkey: event.pubkey?.substring(0, 8) }) logger.debug('[Publish] Determining target relays...', { kind: event.kind, pubkey: event.pubkey?.substring(0, 8) })
@ -1658,6 +1659,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const deletionRequest = await signEvent(createDeletionRequestDraftEvent(targetEvent)) const deletionRequest = await signEvent(createDeletionRequestDraftEvent(targetEvent))
client.interruptBackgroundQueries()
// Privacy: Only use user's own relays, never connect to "seen on" relays // Privacy: Only use user's own relays, never connect to "seen on" relays
const favUrls = favoriteRelayUrlsForPublish(favoriteRelaysEvent, account?.pubkey ?? null) const favUrls = favoriteRelayUrlsForPublish(favoriteRelaysEvent, account?.pubkey ?? null)
const relays = await client.determineTargetRelays(targetEvent, { const relays = await client.determineTargetRelays(targetEvent, {

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

@ -222,6 +222,16 @@ export interface QueryOptions {
firstRelayResultGraceMs?: number | false firstRelayResultGraceMs?: number | false
/** Label for {@link RelaySubscribeOpBatch} when this query opens REQs. */ /** Label for {@link RelaySubscribeOpBatch} when this query opens REQs. */
relayOpSource?: string relayOpSource?: string
/**
* When aborted (e.g. React effect cleanup / HMR), closes WS + HTTP index work promptly instead of waiting for
* {@link globalTimeout}. Prevents overlapping NIP-50 shards from stacking until the tab OOMs.
*/
signal?: AbortSignal
/**
* When true, this query ignores {@link QueryService.interruptBackgroundQueries} (e.g. NIP-50 shard with its own
* AbortController, or other foreground work that must not be tied to the global background token).
*/
foreground?: boolean
} }
export interface SubscribeCallbacks { export interface SubscribeCallbacks {
@ -255,6 +265,21 @@ export class QueryService {
private globalRelayConnectionSlotsInUse = 0 private globalRelayConnectionSlotsInUse = 0
private globalRelayConnectionWaitQueue: Array<() => void> = [] private globalRelayConnectionWaitQueue: Array<() => void> = []
/**
* Aborted whenever {@link interruptBackgroundQueries} runs. Default {@link query} runs listen until close so
* feed / prefetch / replaceable fetches yield to search and publish.
*/
private backgroundInterruptController = new AbortController()
/**
* Best-effort: abort in-flight {@link query} calls that did not pass `foreground: true`, then reset the token so
* new background work uses a fresh signal.
*/
interruptBackgroundQueries(): void {
this.backgroundInterruptController.abort()
this.backgroundInterruptController = new AbortController()
}
async acquireGlobalRelayConnectionSlot(): Promise<void> { async acquireGlobalRelayConnectionSlot(): Promise<void> {
if (this.globalRelayConnectionSlotsInUse < MAX_CONCURRENT_RELAY_CONNECTIONS) { if (this.globalRelayConnectionSlotsInUse < MAX_CONCURRENT_RELAY_CONNECTIONS) {
this.globalRelayConnectionSlotsInUse++ this.globalRelayConnectionSlotsInUse++
@ -359,6 +384,7 @@ export class QueryService {
): Promise<NEvent[]> { ): Promise<NEvent[]> {
const sanitizedFilters = sanitizeFiltersBeforeReq(filter) const sanitizedFilters = sanitizeFiltersBeforeReq(filter)
if (sanitizedFilters.length === 0) return [] if (sanitizedFilters.length === 0) return []
if (options?.signal?.aborted) return []
const maxFilters = RELAY_REQ_MAX_FILTERS_PER_MESSAGE const maxFilters = RELAY_REQ_MAX_FILTERS_PER_MESSAGE
if (sanitizedFilters.length > maxFilters) { if (sanitizedFilters.length > maxFilters) {
@ -418,10 +444,12 @@ export class QueryService {
const reqId = ++queryReqSeq const reqId = ++queryReqSeq
const source = options?.relayOpSource ?? 'QueryService.query' const source = options?.relayOpSource ?? 'QueryService.query'
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 foreground = options?.foreground === true
return await new Promise<NEvent[]>((resolve) => { return await new Promise<NEvent[]>((resolve) => {
const events: NEvent[] = [] const events: NEvent[] = []
const cancelAbortRegistrations: Array<() => void> = []
const abortHttp = new AbortController() const abortHttp = new AbortController()
let resolveTimeout: ReturnType<typeof setTimeout> | null = null let resolveTimeout: ReturnType<typeof setTimeout> | null = null
let firstResultGraceTimeoutId: ReturnType<typeof setTimeout> | null = null let firstResultGraceTimeoutId: ReturnType<typeof setTimeout> | null = null
@ -523,6 +551,10 @@ export class QueryService {
*/ */
const finalizeOnce = () => { const finalizeOnce = () => {
if (resolved) return if (resolved) return
for (const detach of cancelAbortRegistrations) {
detach()
}
cancelAbortRegistrations.length = 0
resolved = true resolved = true
if (resolveTimeout) clearTimeout(resolveTimeout) if (resolveTimeout) clearTimeout(resolveTimeout)
if (firstResultGraceTimeoutId) clearTimeout(firstResultGraceTimeoutId) if (firstResultGraceTimeoutId) clearTimeout(firstResultGraceTimeoutId)
@ -664,6 +696,29 @@ export class QueryService {
} }
} }
const onAbortQuery = () => {
sub.close()
resolveWithEvents()
}
const registerQueryAbort = (sig: AbortSignal) => {
if (sig.aborted) {
queueMicrotask(() => {
onAbortQuery()
})
return
}
sig.addEventListener('abort', onAbortQuery, { once: true })
cancelAbortRegistrations.push(() => {
sig.removeEventListener('abort', onAbortQuery)
})
}
if (!foreground) {
registerQueryAbort(this.backgroundInterruptController.signal)
}
if (options?.signal) {
registerQueryAbort(options.signal)
}
globalTimeoutId = setTimeout(() => resolveWithEvents(), globalTimeout) globalTimeoutId = setTimeout(() => resolveWithEvents(), globalTimeout)
}) })
} }

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

@ -185,8 +185,6 @@ export class ReplaceableEventService {
d?: string, d?: string,
containingEventRelays: string[] = [] containingEventRelays: string[] = []
): Promise<NEvent | undefined> { ): Promise<NEvent | undefined> {
const cacheKey = d ? `${kind}:${pubkey}:${d}` : `${kind}:${pubkey}`
try { try {
if (kind === kinds.Metadata && !d) { if (kind === kinds.Metadata && !d) {
const sessionEv = client.eventService.getSessionMetadataForPubkey(pubkey) const sessionEv = client.eventService.getSessionMetadataForPubkey(pubkey)

11
src/services/client.service.ts

@ -3136,6 +3136,11 @@ class ClientService extends EventTarget {
set.add(relay) set.add(relay)
} }
/** Yield relay pool / HTTP index capacity to search or publish by aborting default {@link QueryService.query} work. */
interruptBackgroundQueries(): void {
this.queryService.interruptBackgroundQueries()
}
// Delegate to QueryService // Delegate to QueryService
private async query( private async query(
urls: string[], urls: string[],
@ -3228,7 +3233,7 @@ class ClientService extends EventTarget {
async fetchEventsFromSingleRelay( async fetchEventsFromSingleRelay(
url: string, url: string,
filter: Filter | Filter[], filter: Filter | Filter[],
options?: { globalTimeout?: number } options?: { globalTimeout?: number; signal?: AbortSignal }
): Promise<{ events: NEvent[]; connectionError?: string }> { ): Promise<{ events: NEvent[]; connectionError?: string }> {
const normalized = normalizeAnyRelayUrl(url) || url const normalized = normalizeAnyRelayUrl(url) || url
if (!normalized) { if (!normalized) {
@ -3236,7 +3241,9 @@ class ClientService extends EventTarget {
} }
const queryOpts = { const queryOpts = {
globalTimeout: options?.globalTimeout ?? 25_000, globalTimeout: options?.globalTimeout ?? 25_000,
relayOpSource: 'fetchEventsFromSingleRelay' as const relayOpSource: 'fetchEventsFromSingleRelay' as const,
foreground: true as const,
...(options?.signal ? { signal: options.signal } : {})
} }
if (isHttpRelayUrl(normalized)) { if (isHttpRelayUrl(normalized)) {

Loading…
Cancel
Save