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

12
src/components/SearchResult/index.tsx

@ -7,8 +7,9 @@ import { ProfileListBySearch } from '../ProfileListBySearch' @@ -7,8 +7,9 @@ import { ProfileListBySearch } from '../ProfileListBySearch'
import Relay from '../Relay'
import { useNostr } from '@/providers/NostrProvider'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import client from '@/services/client.service'
import { normalizeUrl } from '@/lib/url'
import { useMemo } from 'react'
import { useLayoutEffect, useMemo } from 'react'
function relayDedupeKey(url: string): string {
return (normalizeUrl(url) || url.trim()).toLowerCase()
@ -18,6 +19,13 @@ export default function SearchResult({ searchParams }: { searchParams: TSearchPa @@ -18,6 +19,13 @@ export default function SearchResult({ searchParams }: { searchParams: TSearchPa
const { pubkey, relayList } = useNostr()
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. */
const searchableUrls = useMemo(
() =>
@ -34,7 +42,7 @@ export default function SearchResult({ searchParams }: { searchParams: TSearchPa @@ -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)
const combinedRelays = useMemo(() => {
let relays: string[] = []
const relays: string[] = []
if (relayList) {
relays.push(...(relayList.read || []), ...(relayList.write || []))

3
src/providers/NostrProvider/index.tsx

@ -1537,6 +1537,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -1537,6 +1537,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
}
}
client.interruptBackgroundQueries()
noteStatsService.beginPublishPriority()
try {
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 }) { @@ -1658,6 +1659,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const deletionRequest = await signEvent(createDeletionRequestDraftEvent(targetEvent))
client.interruptBackgroundQueries()
// Privacy: Only use user's own relays, never connect to "seen on" relays
const favUrls = favoriteRelayUrlsForPublish(favoriteRelaysEvent, account?.pubkey ?? null)
const relays = await client.determineTargetRelays(targetEvent, {

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

@ -222,6 +222,16 @@ export interface QueryOptions { @@ -222,6 +222,16 @@ export interface QueryOptions {
firstRelayResultGraceMs?: number | false
/** Label for {@link RelaySubscribeOpBatch} when this query opens REQs. */
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 {
@ -255,6 +265,21 @@ export class QueryService { @@ -255,6 +265,21 @@ export class QueryService {
private globalRelayConnectionSlotsInUse = 0
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> {
if (this.globalRelayConnectionSlotsInUse < MAX_CONCURRENT_RELAY_CONNECTIONS) {
this.globalRelayConnectionSlotsInUse++
@ -359,6 +384,7 @@ export class QueryService { @@ -359,6 +384,7 @@ export class QueryService {
): Promise<NEvent[]> {
const sanitizedFilters = sanitizeFiltersBeforeReq(filter)
if (sanitizedFilters.length === 0) return []
if (options?.signal?.aborted) return []
const maxFilters = RELAY_REQ_MAX_FILTERS_PER_MESSAGE
if (sanitizedFilters.length > maxFilters) {
@ -418,10 +444,12 @@ export class QueryService { @@ -418,10 +444,12 @@ export class QueryService {
const reqId = ++queryReqSeq
const source = options?.relayOpSource ?? 'QueryService.query'
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) => {
const events: NEvent[] = []
const cancelAbortRegistrations: Array<() => void> = []
const abortHttp = new AbortController()
let resolveTimeout: ReturnType<typeof setTimeout> | null = null
let firstResultGraceTimeoutId: ReturnType<typeof setTimeout> | null = null
@ -523,6 +551,10 @@ export class QueryService { @@ -523,6 +551,10 @@ export class QueryService {
*/
const finalizeOnce = () => {
if (resolved) return
for (const detach of cancelAbortRegistrations) {
detach()
}
cancelAbortRegistrations.length = 0
resolved = true
if (resolveTimeout) clearTimeout(resolveTimeout)
if (firstResultGraceTimeoutId) clearTimeout(firstResultGraceTimeoutId)
@ -664,6 +696,29 @@ export class QueryService { @@ -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)
})
}

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

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

11
src/services/client.service.ts

@ -3136,6 +3136,11 @@ class ClientService extends EventTarget { @@ -3136,6 +3136,11 @@ class ClientService extends EventTarget {
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
private async query(
urls: string[],
@ -3228,7 +3233,7 @@ class ClientService extends EventTarget { @@ -3228,7 +3233,7 @@ class ClientService extends EventTarget {
async fetchEventsFromSingleRelay(
url: string,
filter: Filter | Filter[],
options?: { globalTimeout?: number }
options?: { globalTimeout?: number; signal?: AbortSignal }
): Promise<{ events: NEvent[]; connectionError?: string }> {
const normalized = normalizeAnyRelayUrl(url) || url
if (!normalized) {
@ -3236,7 +3241,9 @@ class ClientService extends EventTarget { @@ -3236,7 +3241,9 @@ class ClientService extends EventTarget {
}
const queryOpts = {
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)) {

Loading…
Cancel
Save