Browse Source

restore relay strike system, fix search

imwald
Silberengel 1 month ago
parent
commit
c6d81c485a
  1. 17
      src/components/NoteList/index.tsx
  2. 79
      src/components/SearchResult/index.tsx
  3. 6
      src/i18n/locales/de.ts
  4. 4
      src/i18n/locales/en.ts
  5. 17
      src/lib/event.ts
  6. 5
      src/lib/relay-list-builder.ts
  7. 13
      src/pages/secondary/NoteListPage/index.tsx
  8. 5
      src/pages/secondary/NotePage/index.tsx
  9. 22
      src/services/client-events.service.ts
  10. 5
      src/services/client.service.ts
  11. 44
      src/services/indexed-db.service.ts
  12. 58
      src/services/mention-event-search.service.ts

17
src/components/NoteList/index.tsx

@ -3483,8 +3483,6 @@ const NoteList = forwardRef( @@ -3483,8 +3483,6 @@ const NoteList = forwardRef(
if (!timelinePublicReadFallback) return
if (oneShotFetch || areAlgoRelays) return
if (!navigator.onLine) return
const warm = progressiveWarmupQuery?.trim()
if (warm) return
if (feedFullSearchEvents !== null) return
if (feedSubscribeRelayOutcomes.length === 0) return
if (publicReadFallbackAttemptedRef.current) return
@ -3492,11 +3490,22 @@ const NoteList = forwardRef( @@ -3492,11 +3490,22 @@ const NoteList = forwardRef(
const uiStatuses = relayOpTerminalRowsToTimelineRelayUiStatuses(feedSubscribeRelayOutcomes)
if (uiStatuses.some((s) => s.success)) return
publicReadFallbackAttemptedRef.current = true
const mapped = mapLiveSubRequestsForTimeline(subRequestsRef.current)
if (!mapped.length) return
// Skip fallback for d-tag / layered warmup feeds where the live REQ has no NIP-50 `search`
// (merging unfiltered FAST_READ would flood the list). Nostr text search passes `search` on
// the same filter as {@link progressiveWarmupQuery} — allow fallback there.
const warm = progressiveWarmupQuery?.trim()
if (warm) {
const primaryFilter = mapped[0]!.filter as Filter
const hasNip50Search =
typeof primaryFilter.search === 'string' && primaryFilter.search.trim().length > 0
if (!hasNip50Search) return
}
publicReadFallbackAttemptedRef.current = true
const filter: Filter = { ...(mapped[0]!.filter as Filter) }
if (!filter.kinds?.length) {
filter.kinds = effectiveShowKinds.length > 0 ? [...effectiveShowKinds] : [kinds.ShortTextNote]

79
src/components/SearchResult/index.tsx

@ -15,36 +15,61 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' @@ -15,36 +15,61 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { normalizeUrl } from '@/lib/url'
import { useMemo } from 'react'
function relayDedupeKey(url: string): string {
return (normalizeUrl(url) || url.trim()).toLowerCase()
}
export default function SearchResult({ searchParams }: { searchParams: TSearchParams | null }) {
const { pubkey, relayList } = useNostr()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
// Build comprehensive relay list for search (all available relays)
const searchRelays = useMemo(() => {
/** NIP-50 / index relays — always queried first on their own shard so dead personal relays cannot zero out search. */
const searchableUrls = useMemo(
() =>
Array.from(
new Set(SEARCHABLE_RELAY_URLS.map((u) => normalizeUrl(u) || u.trim()).filter(Boolean))
),
[]
)
const searchableKeySet = useMemo(
() => new Set(searchableUrls.map(relayDedupeKey)),
[searchableUrls]
)
// User stack + defaults (full list for second subRequest; excludes searchable URLs to avoid duplicate sockets)
const combinedRelays = useMemo(() => {
let relays: string[] = []
// User's relays
if (relayList) {
relays.push(...(relayList.read || []), ...(relayList.write || []))
}
// User's favorite relays
relays.push(...(favoriteRelays || []))
// All default relays
relays.push(...FAST_READ_RELAY_URLS, ...FAST_WRITE_RELAY_URLS, ...SEARCHABLE_RELAY_URLS)
// Normalize and deduplicate
const normalized = Array.from(new Set(
relays.map(url => normalizeUrl(url) || url).filter((url): url is string => !!url)
))
const normalized = Array.from(
new Set(relays.map((url) => normalizeUrl(url) || url).filter((url): url is string => !!url))
)
// Filter blocked
return normalized.filter(relay =>
!blockedRelays.some(blocked => relay.includes(blocked))
const blockedSet = new Set(
(blockedRelays ?? [])
.map((b) => normalizeUrl(b) || b.trim())
.filter((b): b is string => !!b)
)
return normalized.filter((relay) => {
const n = normalizeUrl(relay) || relay
return !blockedSet.has(n)
})
}, [pubkey, relayList, favoriteRelays, blockedRelays])
const nonSearchableRelays = useMemo(
() => combinedRelays.filter((u) => !searchableKeySet.has(relayDedupeKey(u))),
[combinedRelays, searchableKeySet]
)
if (!searchParams) {
return null
}
@ -55,20 +80,21 @@ export default function SearchResult({ searchParams }: { searchParams: TSearchPa @@ -55,20 +80,21 @@ export default function SearchResult({ searchParams }: { searchParams: TSearchPa
return <ProfileListBySearch search={searchParams.search} />
}
if (searchParams.type === 'notes') {
const notesFilter = {
search: searchParams.search,
kinds: [...NIP_SEARCH_PAGE_KINDS],
limit: 100
}
const subRequests = [
{ urls: searchableUrls, filter: notesFilter },
...(nonSearchableRelays.length > 0 ? [{ urls: nonSearchableRelays, filter: notesFilter }] : [])
]
return (
<NormalFeed
subRequests={[
{
urls: searchRelays,
filter: {
search: searchParams.search,
kinds: [...NIP_SEARCH_PAGE_KINDS],
limit: 100
}
}
]}
subRequests={subRequests}
useFilterAsIs
clientSideKindFilter
timelinePublicReadFallback
progressiveWarmupQuery={searchParams.search}
progressiveDocumentKinds={NIP_SEARCH_PAGE_KINDS}
oneShotAfterMergeComparator={(a, b) => compareEventsForDTagQuery(searchParams.search, a, b)}
@ -76,10 +102,13 @@ export default function SearchResult({ searchParams }: { searchParams: TSearchPa @@ -76,10 +102,13 @@ export default function SearchResult({ searchParams }: { searchParams: TSearchPa
)
}
if (searchParams.type === 'hashtag') {
const hashtagFilter = { '#t': [searchParams.search] }
const subRequests = [
{ urls: searchableUrls, filter: hashtagFilter },
...(nonSearchableRelays.length > 0 ? [{ urls: nonSearchableRelays, filter: hashtagFilter }] : [])
]
return (
<NormalFeed
subRequests={[{ urls: searchRelays, filter: { '#t': [searchParams.search] } }]}
/>
<NormalFeed timelinePublicReadFallback subRequests={subRequests} />
)
}
return <Relay url={searchParams.search} />

6
src/i18n/locales/de.ts

@ -603,9 +603,9 @@ export default { @@ -603,9 +603,9 @@ export default {
relayType_contextual: "Antwort/PN",
relayType_randomly_selected: "Zufällig (optional)",
"Session relays": "Session-Relays",
"Session relays tab description": "Relay-Logik für diese Session: funktionierende und gestrichene Preset-Relays sowie bewertete Zufallsrelays. Gestrichene Relays werden für Lesen und Schreiben bis zum Neuladen der App übersprungen.",
"Session relays preset working": "Funktionierende Preset-Relays",
"Session relays preset working hint": "Preset-Relays aus den App-Standards.",
"Session relays tab description": "Relay-Logik für diese Session: eingebaute Preset-Relays sowie Zufallsrelays, die in dieser Session mindestens ein Publish angenommen haben; letztere werden beim Auswählen zufälliger Veröffentlichungsrelays bevorzugt.",
"Session relays preset working": "Eingebaute Preset-Relays",
"Session relays preset working hint": "URLs aus den fest eingebauten Relay-Listen der App (schnell lesen/schreiben, durchsuchbar, Starter-Favoriten) — keine Live-Gesundheitsprüfung und kein „Strich“-Sperren mehr.",
"Session relays scored random": "Bewertete Zufallsrelays",
"Session relays scored random hint": "Relays, die in dieser Session mindestens ein Publish angenommen haben; werden beim Auswählen von Zufallsrelays bevorzugt. Sortiert nach durchschnittlicher Latenz.",
successes: "Erfolge",

4
src/i18n/locales/en.ts

@ -608,8 +608,8 @@ export default { @@ -608,8 +608,8 @@ export default {
relayType_randomly_selected: "Random (optional)",
"Session relays": "Session relays",
"Session relays tab description": "Relay logic for this session: preset relays and scored random relays used to prefer faster, proven relays when adding random relays to publish.",
"Session relays preset working": "Working preset relays",
"Session relays preset working hint": "Preset relays from app defaults.",
"Session relays preset working": "Built-in preset relays",
"Session relays preset working hint": "URLs from the app’s built-in relay lists (fast read/write, searchable, starter favorites). Shown for reference — not a live health check.",
"Session relays scored random": "Scored random relays",
"Session relays scored random hint": "Relays that have accepted at least one publish this session; used to prefer faster relays when picking random relays. Sorted by average latency.",
successes: "successes",

17
src/lib/event.ts

@ -2,6 +2,7 @@ import { ExtendedKind, isNip52CalendarCardKind } from '@/constants' @@ -2,6 +2,7 @@ import { ExtendedKind, isNip52CalendarCardKind } from '@/constants'
import { muteSetHas } from '@/lib/mute-set'
import { EMBEDDED_EVENT_REGEX, EMBEDDED_MENTION_REGEX, NOSTR_EMBEDDED_NOTE_REGEX } from '@/lib/content-patterns'
import { cleanUrl, normalizeUrl } from '@/lib/url'
import { urlIsNonLocalForRemoteViewer } from '@/lib/relay-list-sanitize'
import client from '@/services/client.service'
import { TImetaInfo } from '@/types'
import { LRUCache } from 'lru-cache'
@ -622,25 +623,33 @@ export function collectEmbeddedEventPrefetchTargets(event: Event): { @@ -622,25 +623,33 @@ export function collectEmbeddedEventPrefetchTargets(event: Event): {
*/
export function relayHintWssUrlsFromEvent(event: Event | undefined): string[] {
if (!event) return []
const hints: string[] = []
const fromTags: string[] = []
for (const tag of event.tags) {
if (['e', 'a', 'q'].includes(tag[0]) && tag.length > 2 && typeof tag[2] === 'string') {
const hint = tag[2]
if (hint.startsWith('wss://') || hint.startsWith('ws://')) hints.push(hint)
if (hint.startsWith('wss://') || hint.startsWith('ws://')) {
const n = normalizeUrl(hint) || hint
if (urlIsNonLocalForRemoteViewer(n)) fromTags.push(hint)
}
}
}
const relaysTag = event.tags.find((t) => t[0] === 'relays')
if (relaysTag) {
for (let i = 1; i < relaysTag.length; i++) {
const u = relaysTag[i]
if (typeof u === 'string' && (u.startsWith('wss://') || u.startsWith('ws://'))) hints.push(u)
if (typeof u === 'string' && (u.startsWith('wss://') || u.startsWith('ws://'))) {
const n = normalizeUrl(u) || u
if (urlIsNonLocalForRemoteViewer(n)) fromTags.push(u)
}
}
}
const seen: string[] = []
try {
hints.push(...client.getSeenEventRelayUrls(event.id))
seen.push(...client.getSeenEventRelayUrls(event.id))
} catch {
/* ignore */
}
const hints = [...fromTags, ...seen]
const normalized = hints
.map((u) => normalizeUrl(u))
.filter((u): u is string => Boolean(u))

5
src/lib/relay-list-builder.ts

@ -12,6 +12,7 @@ @@ -12,6 +12,7 @@
import { FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS, PROFILE_FETCH_RELAY_URLS, SEARCHABLE_RELAY_URLS } from '@/constants'
import { feedRelayPolicyUrls } from '@/features/feed/relay-policy'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { urlIsNonLocalForRemoteViewer } from '@/lib/relay-list-sanitize'
import { isHttpRelayUrl, normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url'
import { getCacheRelayUrls } from './private-relays'
import client from '@/services/client.service'
@ -283,13 +284,13 @@ export async function buildExploreProfileAndUserRelayList( @@ -283,13 +284,13 @@ export async function buildExploreProfileAndUserRelayList(
}
}
/** NIP-10 relay hints from `e` / `E` tags (third value) on the focused event or thread. */
/** NIP-10 relay hints from `e` / `E` tags (third value) on the focused event or thread. Omits loopback/LAN — those are only meaningful on the tag author's machine. */
export function relayHintsFromEventTags(event: { tags: string[][] }): string[] {
const out = new Set<string>()
for (const tag of event.tags) {
if ((tag[0] === 'e' || tag[0] === 'E') && tag[2]) {
const n = normalizeUrl(tag[2]) || tag[2]
if (n) out.add(n)
if (n && urlIsNonLocalForRemoteViewer(n)) out.add(n)
}
}
return [...out]

13
src/pages/secondary/NoteListPage/index.tsx

@ -25,6 +25,7 @@ import { useNostr } from '@/providers/NostrProvider' @@ -25,6 +25,7 @@ import { useNostr } from '@/providers/NostrProvider'
import { useInterestListOptional } from '@/providers/interest-list-context'
import client from '@/services/client.service'
import { TFeedSubRequest } from '@/types'
import { normalizeUrl } from '@/lib/url'
import { UserRound, Plus } from 'lucide-react'
import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -238,7 +239,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid @@ -238,7 +239,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
setSubRequests([])
}
} else {
// D-tag browse: NIP-50 search + exact #d REQ (merged), substring match client-side, exact d-tag sorted first
// D-tag browse: exact `#d` REQ on index + user relays (no NIP-50 full-text — that is not the same as a d-tag pick).
setTitle(`D-Tag: ${domain}`)
setData({
type: 'dtag',
@ -255,17 +256,11 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid @@ -255,17 +256,11 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
new Set([...NIP_SEARCH_DOCUMENT_KINDS, ...(kinds.length > 0 ? kinds : [])])
).sort((a, b) => a - b)
const kindFilter = { kinds: mergedReqKinds }
// NIP-50 full-text search works better with natural-language spacing;
// convert the hyphenated slug back to a space-separated query for the search relay.
const searchQuery = domain.replace(/-/g, ' ')
const dUrls = [...new Set([...SEARCHABLE_RELAY_URLS.map((u) => normalizeUrl(u) || u).filter(Boolean), ...relayUrls])]
setSubRequests([
{
filter: { search: searchQuery, ...kindFilter },
urls: [...new Set([...relayUrls, ...SEARCHABLE_RELAY_URLS])]
},
{
filter: { '#d': [domain], ...kindFilter },
urls: relayUrls
urls: dUrls
}
])
}

5
src/pages/secondary/NotePage/index.tsx

@ -510,8 +510,7 @@ const NotePage = forwardRef(({ id, index, hideTitlebar = false, initialEvent }: @@ -510,8 +510,7 @@ const NotePage = forwardRef(({ id, index, hideTitlebar = false, initialEvent }:
<div className="px-4 pt-3 w-full">
{rootITag && <ExternalRoot value={rootITag[1]} />}
{rootEventId &&
!eventPointersReferenceSameNote(rootEventId, parentEventId) &&
(isFetchingRootEvent || rootEventForStrip) && (
!eventPointersReferenceSameNote(rootEventId, parentEventId) && (
<ParentNote
key={`root-note-${finalEvent.id}`}
isFetching={isFetchingRootEvent}
@ -520,7 +519,7 @@ const NotePage = forwardRef(({ id, index, hideTitlebar = false, initialEvent }: @@ -520,7 +519,7 @@ const NotePage = forwardRef(({ id, index, hideTitlebar = false, initialEvent }:
isConsecutive={isConsecutive(rootEventForStrip, parentEventForStrip)}
/>
)}
{parentEventId && (isFetchingParentEvent || parentEventForStrip) && (
{parentEventId && (
<ParentNote
key={`parent-note-${finalEvent.id}`}
isFetching={isFetchingParentEvent}

22
src/services/client-events.service.ts

@ -90,6 +90,9 @@ const PREFETCH_HEX_IDS_CHUNK = 48 @@ -90,6 +90,9 @@ const PREFETCH_HEX_IDS_CHUNK = 48
/** Cap session LRU scan per note-stats target — cache iterates newest-first; avoids O(session)×batch stalls. */
const NOTE_STATS_SESSION_PREMERGE_SCAN_MAX = 6000
/** Max session events scanned for {@link EventService.getSessionEventsMatchingSearch} (Map order is not recency). */
const SESSION_SEARCH_MAX_SCAN = 48_000
export class EventService {
private queryService: QueryService
private eventCacheMap = new Map<string, Promise<NEvent | undefined>>()
@ -650,32 +653,35 @@ export class EventService { @@ -650,32 +653,35 @@ export class EventService {
}
/**
* Get events from session cache matching search
* Get events from session cache matching search (newest {@link Event.created_at} first).
* Scans up to {@link SESSION_SEARCH_MAX_SCAN} entries so LRU insertion order does not hide recent matches.
*/
getSessionEventsMatchingSearch(query: string, limit: number, allowedKinds?: number[]): NEvent[] {
const results: NEvent[] = []
const queryTrim = query.trim()
const queryLower = queryTrim.toLowerCase()
const kindSet = allowedKinds && allowedKinds.length > 0 ? new Set(allowedKinds) : null
const buf: NEvent[] = []
let scanned = 0
for (const [, event] of this.sessionEventCache.entries()) {
if (++scanned > SESSION_SEARCH_MAX_SCAN) break
if (shouldDropEventOnIngest(event)) continue
if (allowedKinds && !allowedKinds.includes(event.kind)) continue
if (kindSet && !kindSet.has(event.kind)) continue
if (queryTrim === '') {
results.push(event)
if (results.length >= limit) break
buf.push(event)
continue
}
const content = (event.content ?? '').toLowerCase()
const tagsStr = (event.tags ?? []).flat().join(' ').toLowerCase()
if (content.includes(queryLower) || tagsStr.includes(queryLower)) {
results.push(event)
if (results.length >= limit) break
buf.push(event)
}
}
return results
buf.sort((a, b) => b.created_at - a.created_at || b.id.localeCompare(a.id))
return buf.slice(0, limit)
}
getSessionEventsMatchingFilters(filters: readonly Filter[], limit: number): NEvent[] {

5
src/services/client.service.ts

@ -3899,7 +3899,10 @@ class ClientService extends EventTarget { @@ -3899,7 +3899,10 @@ class ClientService extends EventTarget {
})
}
return mergeKind10243(relayList)
const merged = mergeKind10243(relayList)
// Kind 10243 can still carry another user's loopback index (e.g. http://localhost:8080). NIP-65 `r` rows
// were stripped above; strip again after HTTP merge for other users' bundles only (viewer keeps 10432/LAN).
return isOwnRelayList ? merged : stripLocalNetworkRelaysFromRelayList(merged)
})
}

44
src/services/indexed-db.service.ts

@ -1412,31 +1412,45 @@ class IndexedDbService { @@ -1412,31 +1412,45 @@ class IndexedDbService {
/**
* Iterate PUBLICATION_EVENTS and return events whose kind is in allowedKinds and content or tags
* match the search query (case-insensitive). Used by nevent/naddr picker to show cached events first.
* match the query (case-insensitive). Scans up to `scanBudget` rows and keeps up to `collectCap` matches,
* then returns the newest `limit` by {@link Event.created_at} (cursor order alone is not recency).
*/
async getCachedEventsForSearch(query: string, limit: number, allowedKinds: number[]): Promise<Event[]> {
async getCachedEventsForSearch(
query: string,
limit: number,
allowedKinds: number[],
options?: { scanBudget?: number; collectCap?: number }
): Promise<Event[]> {
await this.initPromise
if (!this.db || !this.db.objectStoreNames.contains(StoreNames.PUBLICATION_EVENTS)) {
return []
}
const q = query.trim().toLowerCase()
if (!q || allowedKinds.length === 0) return []
if (!q || allowedKinds.length === 0 || limit <= 0) return []
const kindSet = new Set(allowedKinds)
const scanBudget = Math.min(Math.max(options?.scanBudget ?? 28_000, 400), 120_000)
const collectCap = Math.min(
Math.max(options?.collectCap ?? Math.max(limit * 8, limit + 200, 200), limit),
12_000
)
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction(StoreNames.PUBLICATION_EVENTS, 'readonly')
const store = transaction.objectStore(StoreNames.PUBLICATION_EVENTS)
const request = store.openCursor()
const results: Event[] = []
let scanned = 0
request.onsuccess = () => {
const cursor = (request as IDBRequest<IDBCursorWithValue>).result
if (!cursor || results.length >= limit) {
if (!cursor || scanned >= scanBudget || results.length >= collectCap) {
transaction.commit()
resolve(results)
results.sort((a, b) => b.created_at - a.created_at || b.id.localeCompare(a.id))
resolve(results.slice(0, limit))
return
}
scanned += 1
const item = cursor.value as TValue<Event> | undefined
if (item?.value) {
const event = item.value as Event
@ -1575,11 +1589,17 @@ class IndexedDbService { @@ -1575,11 +1589,17 @@ class IndexedDbService {
allowedKinds: number[],
options?: { archiveScanMaxMs?: number }
): Promise<Event[]> {
const fromPub = await this.getCachedEventsForSearch(query, limit, allowedKinds)
if (fromPub.length >= limit) return fromPub.slice(0, limit)
const pubCap = Math.min(900, Math.max(limit * 6, limit + 280, 220))
const fromPub = await this.getCachedEventsForSearch(query, pubCap, allowedKinds, {
scanBudget: 70_000,
collectCap: Math.min(10_000, pubCap * 12)
})
if (fromPub.length >= pubCap) {
return fromPub.slice(0, limit)
}
const q = query.trim().toLowerCase()
if (!q || allowedKinds.length === 0) return fromPub
if (!q || allowedKinds.length === 0) return fromPub.slice(0, limit)
const kindSet = new Set(allowedKinds)
const seen = new Set(fromPub.map((e) => e.id))
@ -1589,7 +1609,7 @@ class IndexedDbService { @@ -1589,7 +1609,7 @@ class IndexedDbService {
await this.initPromise
if (!this.db?.objectStoreNames.contains(StoreNames.EVENT_ARCHIVE)) {
return fromPub
return fromPub.slice(0, limit)
}
await new Promise<void>((resolve, reject) => {
@ -1607,7 +1627,7 @@ class IndexedDbService { @@ -1607,7 +1627,7 @@ class IndexedDbService {
return
}
const cursor = (request as IDBRequest<IDBCursorWithValue>).result
if (!cursor || fromPub.length + rest.length >= limit) {
if (!cursor || fromPub.length + rest.length >= pubCap) {
transaction.commit()
resolve()
return
@ -1633,7 +1653,9 @@ class IndexedDbService { @@ -1633,7 +1653,9 @@ class IndexedDbService {
logger.warn('[indexedDb] getCachedAndArchivedEventsMatchingLocalSearch archive scan failed', { e })
})
return [...fromPub, ...rest].slice(0, limit)
const merged = [...fromPub, ...rest]
merged.sort((a, b) => b.created_at - a.created_at || b.id.localeCompare(a.id))
return merged.slice(0, limit)
}
/**

58
src/services/mention-event-search.service.ts

@ -9,6 +9,7 @@ import { @@ -9,6 +9,7 @@ import {
tryParseCitationEventIdFromQuery
} from '@/lib/citation-picker-search'
import { ExtendedKind, NIP71_VIDEO_KINDS, SEARCHABLE_RELAY_URLS } from '@/constants'
import { normalizeUrl } from '@/lib/url'
import { kinds, type Event as NEvent } from 'nostr-tools'
import client, { eventService, queryService } from './client.service'
import indexedDb from './indexed-db.service'
@ -138,8 +139,13 @@ async function searchCitationEventsForPickerInternal( @@ -138,8 +139,13 @@ async function searchCitationEventsForPickerInternal(
return out.slice(0, limit)
}
/** Local DB + session budget for picker search (before relay NIP-50). */
const PICKER_LOCAL_DB_MERGE_CAP = 880
const PICKER_FULLTEXT_DB_CAP = 260
/**
* Search for events: session cache IndexedDB relays. Merges and dedupes by event id, up to limit.
* Search for events: session cache IndexedDB (publication + archive + cross-store full text) relays.
* Merges and dedupes by event id, up to limit.
* @param mode - 'nevent' uses NEVENT_KINDS (incl. NIP-71 video 21/22/34235/34236), 'naddr' uses NADDR_KINDS (30023,30817,30818,30040).
* @param kindFilter - When set, only these kinds are searched (overrides `mode` for the kinds list).
*/
@ -168,22 +174,58 @@ export async function searchEventsForPicker( @@ -168,22 +174,58 @@ export async function searchEventsForPicker(
out.push(evt)
}
const fromSession = eventService.getSessionEventsMatchingSearch(q, limit, kindsList)
const sessionCap = Math.min(1500, Math.max(limit * 8, 200))
const fromSession = eventService.getSessionEventsMatchingSearch(q, sessionCap, kindsList)
fromSession.forEach(addUnique)
if (out.length >= limit) return out.slice(0, limit)
const localMergeTarget = Math.min(PICKER_LOCAL_DB_MERGE_CAP, Math.max(limit * 10, 240))
const [fromLocalDb, userCentricRelayUrls] = await Promise.all([
indexedDb.getCachedAndArchivedEventsMatchingLocalSearch(q, localMergeTarget, kindsList, {
archiveScanMaxMs: 24_000
}),
buildCitationPickerSearchRelayUrls()
])
fromLocalDb.forEach(addUnique)
try {
const fullTextHits = await indexedDb.searchAllCachedEventsFullText(q, {
limit: Math.min(PICKER_FULLTEXT_DB_CAP, Math.max(localMergeTarget, 200))
})
const kindSet = new Set(kindsList)
for (const hit of fullTextHits) {
const ev = hit.value
if (ev && kindSet.has(ev.kind)) addUnique(ev as NEvent)
if (out.length >= limit) break
}
} catch {
/* best-effort: other stores optional */
}
if (out.length >= limit) return out.slice(0, limit)
const need = limit - out.length
const userCentricRelayUrls = await buildCitationPickerSearchRelayUrls()
const [fromIdb, fromRelays] = await Promise.all([
indexedDb.getCachedEventsForSearch(q, need, kindsList),
const searchableNip50Layer = Array.from(
new Set(SEARCHABLE_RELAY_URLS.map((u) => normalizeUrl(u) || u.trim()).filter(Boolean))
).slice(0, 28)
const [fromUserCentric, fromSearchableIndex] = await Promise.all([
queryService.fetchEvents(
userCentricRelayUrls,
{ kinds: kindsList, search: q, limit: need },
{ eoseTimeout: 5000, globalTimeout: 8000 }
)
),
searchableNip50Layer.length > 0
? queryService.fetchEvents(
searchableNip50Layer,
{ kinds: kindsList, search: q, limit: need },
{ eoseTimeout: 6500, globalTimeout: 12_000 }
)
: Promise.resolve([] as NEvent[])
])
fromIdb.forEach(addUnique)
fromRelays.forEach(addUnique)
fromUserCentric.forEach(addUnique)
fromSearchableIndex.forEach(addUnique)
return out.slice(0, limit)
}

Loading…
Cancel
Save