{rootITag &&
}
{rootEventId &&
- !eventPointersReferenceSameNote(rootEventId, parentEventId) &&
- (isFetchingRootEvent || rootEventForStrip) && (
+ !eventPointersReferenceSameNote(rootEventId, parentEventId) && (
)}
- {parentEventId && (isFetchingParentEvent || parentEventForStrip) && (
+ {parentEventId && (
>()
@@ -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[] {
diff --git a/src/services/client.service.ts b/src/services/client.service.ts
index 9e4026c2..23205a3e 100644
--- a/src/services/client.service.ts
+++ b/src/services/client.service.ts
@@ -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)
})
}
diff --git a/src/services/indexed-db.service.ts b/src/services/indexed-db.service.ts
index cebff045..29da5a3d 100644
--- a/src/services/indexed-db.service.ts
+++ b/src/services/indexed-db.service.ts
@@ -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 {
+ async getCachedEventsForSearch(
+ query: string,
+ limit: number,
+ allowedKinds: number[],
+ options?: { scanBudget?: number; collectCap?: number }
+ ): Promise {
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).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 | undefined
if (item?.value) {
const event = item.value as Event
@@ -1575,11 +1589,17 @@ class IndexedDbService {
allowedKinds: number[],
options?: { archiveScanMaxMs?: number }
): Promise {
- 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 {
await this.initPromise
if (!this.db?.objectStoreNames.contains(StoreNames.EVENT_ARCHIVE)) {
- return fromPub
+ return fromPub.slice(0, limit)
}
await new Promise((resolve, reject) => {
@@ -1607,7 +1627,7 @@ class IndexedDbService {
return
}
const cursor = (request as IDBRequest).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 {
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)
}
/**
diff --git a/src/services/mention-event-search.service.ts b/src/services/mention-event-search.service.ts
index 9e68c320..d82b814a 100644
--- a/src/services/mention-event-search.service.ts
+++ b/src/services/mention-event-search.service.ts
@@ -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(
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(
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)
}