Browse Source

make publication search more comprehensive

remove damus relays
imwald
Silberengel 7 days ago
parent
commit
8720b203d0
  1. 2
      nip66-cron/index.mjs
  2. 4
      package-lock.json
  3. 2
      package.json
  4. 5
      src/components/Embedded/EmbeddedNote.tsx
  5. 2
      src/constants.ts
  6. 39
      src/lib/index-relay-http.ts
  7. 56
      src/lib/library-publication-index.test.ts
  8. 385
      src/lib/library-publication-index.ts
  9. 7
      src/lib/metadata-policy-curated-relays.test.ts
  10. 1
      src/lib/new-user-template-broadcast.ts
  11. 3
      src/lib/new-user-template.ts
  12. 2
      src/services/nip89.service.ts

2
nip66-cron/index.mjs

@ -51,7 +51,6 @@ const DEFAULT_RELAYS_TO_MONITOR = [ @@ -51,7 +51,6 @@ const DEFAULT_RELAYS_TO_MONITOR = [
'wss://nostr.wine',
'wss://nostr21.com',
'wss://aggr.nostr.land',
'wss://relay.damus.io',
'wss://relay.primal.net',
'wss://nos.lol',
'wss://relay.gifbuddy.lol',
@ -75,7 +74,6 @@ const DEFAULT_RELAYS_TO_MONITOR = [ @@ -75,7 +74,6 @@ const DEFAULT_RELAYS_TO_MONITOR = [
/** Relays to publish 30166/10166 and to REQ kind 10002 from; broad enough for Imwald + NIP-66 discovery. */
const DEFAULT_PUBLISH_RELAYS = [
'wss://nos.lol',
'wss://relay.damus.io',
'wss://relay.nostr.watch',
'wss://relay.primal.net',
'wss://relaypag.es',

4
package-lock.json generated

@ -1,12 +1,12 @@ @@ -1,12 +1,12 @@
{
"name": "imwald",
"version": "23.21.5",
"version": "23.21.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "imwald",
"version": "23.21.5",
"version": "23.21.6",
"license": "MIT",
"dependencies": {
"@asciidoctor/core": "^3.0.4",

2
package.json

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
{
"name": "imwald",
"version": "23.21.5",
"version": "23.21.6",
"description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery",
"private": true,
"type": "module",

5
src/components/Embedded/EmbeddedNote.tsx

@ -552,9 +552,8 @@ function preferPublicIndexRelaysFirst(urls: readonly string[]): string[] { @@ -552,9 +552,8 @@ function preferPublicIndexRelaysFirst(urls: readonly string[]): string[] {
const x = u.toLowerCase()
if (x.includes('nos.lol')) return 0
if (x.includes('nostr.land')) return 1
if (x.includes('relay.damus.io')) return 2
if (x.includes('relay.primal.net')) return 3
if (x.includes('nostr.wine')) return 4
if (x.includes('relay.primal.net')) return 2
if (x.includes('nostr.wine')) return 3
return 30
}
return [...urls].sort((a, b) => score(a) - score(b) || a.localeCompare(b))

2
src/constants.ts

@ -536,7 +536,6 @@ export const FAST_READ_RELAY_URLS = [ @@ -536,7 +536,6 @@ export const FAST_READ_RELAY_URLS = [
// Optimized relay list for write operations (no aggregator since it's read-only)
export const FAST_WRITE_RELAY_URLS = [
'wss://relay.damus.io',
'wss://relay.primal.net',
'wss://thecitadel.nostr1.com',
'wss://nos.lol'
@ -585,7 +584,6 @@ export const SEARCH_QUERY_DEBOUNCE_MS = 550 @@ -585,7 +584,6 @@ export const SEARCH_QUERY_DEBOUNCE_MS = 550
export const PROFILE_RELAY_URLS = [
'wss://profiles.nostr1.com',
'wss://relay.damus.io',
'wss://thecitadel.nostr1.com',
'wss://indexer.coracle.social/',
'wss://purplepag.es'

39
src/lib/index-relay-http.ts

@ -30,6 +30,10 @@ function indexRelayPublishUrl(baseUrl: string): string { @@ -30,6 +30,10 @@ function indexRelayPublishUrl(baseUrl: string): string {
return `${trimSlash(normalizeHttpRelayUrl(baseUrl) || baseUrl)}/api/events`
}
function indexRelayPublicationMetadataSearchUrl(baseUrl: string): string {
return `${trimSlash(normalizeHttpRelayUrl(baseUrl) || baseUrl)}/api/publications/search`
}
/** Map a Nostr filter to gc_index_relay POST body (requires `limit` 1–100; strips unsupported keys). */
function nostrFilterToIndexRelayBody(f: Filter): Record<string, unknown> {
const body: Record<string, unknown> = {}
@ -418,19 +422,37 @@ export async function queryIndexRelayForLibrary( @@ -418,19 +422,37 @@ export async function queryIndexRelayForLibrary(
}
}
/** Kind-30040 discovery search: keeps NIP-50 `search` (unlike bulk {@link queryIndexRelayForLibrary}). */
/** Kind-30040 filter query via POST /api/events/filter (NIP-01 only — no NIP-50 `search`). */
export async function queryIndexRelayPublicationSearch(
baseUrl: string,
filter: Filter,
options?: { signal?: AbortSignal }
): Promise<TIndexRelayLibraryPage> {
return queryIndexRelayForLibrary(baseUrl, filter, options)
}
function filterForIndexRelay(f: Filter): Filter {
const rest = { ...f } as Filter & { search?: unknown }
delete rest.search
return rest as Filter
}
/** Kind-30040 metadata search (d / title / author / source) on Mercury-style index relays. */
export async function queryIndexRelayPublicationMetadataSearch(
baseUrl: string,
query: string,
options?: { limit?: number; signal?: AbortSignal }
): Promise<TIndexRelayLibraryPage> {
const q = query.trim()
if (!q) return { events: [], apiRowCount: 0 }
const base = devHttpIndexRelayBaseForFetch(baseUrl)
const endpoint = indexRelayFilterUrl(base)
const endpoint = indexRelayPublicationMetadataSearchUrl(base)
if (shouldSkipDevIndexRelayFetch(endpoint)) {
return { events: [], apiRowCount: 0 }
}
const body = nostrFilterToIndexRelayBody(filter)
const limit = Math.max(1, Math.min(options?.limit ?? 100, 100))
try {
const res = await fetchWithTimeout(endpoint, {
method: 'POST',
@ -438,11 +460,12 @@ export async function queryIndexRelayPublicationSearch( @@ -438,11 +460,12 @@ export async function queryIndexRelayPublicationSearch(
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(body),
body: JSON.stringify({ q, limit }),
signal: options?.signal,
timeoutMs: 25_000
})
if (!res.ok) {
if (res.status === 404 || res.status === 405) return { events: [], apiRowCount: 0 }
if (res.status >= 500) {
markDevIndexRelayUnavailableFromHttpStatus(res.status, endpoint)
throw new IndexRelayTransportError(new Error(`HTTP ${res.status}`))
@ -472,7 +495,7 @@ export async function queryIndexRelayPublicationSearch( @@ -472,7 +495,7 @@ export async function queryIndexRelayPublicationSearch(
handleFilterTransportFailure(endpoint, e)
throw new IndexRelayTransportError(e)
}
warnIndexRelayHttpThrottled(endpoint, '[IndexRelayHttp] publication search request error', {
warnIndexRelayHttpThrottled(endpoint, '[IndexRelayHttp] publication metadata search request error', {
endpoint,
error: e
})
@ -480,12 +503,6 @@ export async function queryIndexRelayPublicationSearch( @@ -480,12 +503,6 @@ export async function queryIndexRelayPublicationSearch(
}
}
function filterForIndexRelay(f: Filter): Filter {
const rest = { ...f } as Filter & { search?: unknown }
delete rest.search
return rest as Filter
}
export async function publishEventToHttpRelay(
baseUrl: string,
event: NEvent,

56
src/lib/library-publication-index.test.ts

@ -3,16 +3,19 @@ import { ExtendedKind } from '@/constants' @@ -3,16 +3,19 @@ import { ExtendedKind } from '@/constants'
import {
buildEngagementMapsFromEvents,
buildLibraryPublicationRelaySearchFilters,
buildLibraryPublicationRelaySearchFiltersForAxis,
buildRecentPublicationEntries,
clearLibrarySearchSessionCache,
computeLibraryFeedRootOrder,
filterEngagedPublications,
filterEventsForPublicationRelaySearchAxis,
filterLibraryPublicationsBySearch,
filterLibraryPublicationsByUser,
libraryDefaultFeedSlice,
libraryPublicationEntriesForUserFromIndex,
LIBRARY_PAGE_SIZE,
pickLibraryPublicationEntries,
publicationMetadataTagMatchesQuery,
publicationRootBelongsToUser,
peekLibrarySearchResults,
publicationIndexMatchesSearchQuery,
@ -233,20 +236,53 @@ describe('library-publication-index', () => { @@ -233,20 +236,53 @@ describe('library-publication-index', () => {
expect(publicationIndexMatchesSearchQuery(root, 'missing')).toBe(false)
})
it('buildLibraryPublicationRelaySearchFilters uses kind 30040 for d-tag and search', () => {
it('buildLibraryPublicationRelaySearchFilters splits kind 30040 into d-tag, title, and author without NIP-50', () => {
expect(publicationQueryDTagVariants('Village Life in China')).toContain('village-life-in-china')
const filters = buildLibraryPublicationRelaySearchFilters({ query: 'Village Life in China' })
expect(filters.length).toBeGreaterThan(0)
expect(filters.every((f) => f.kinds?.length === 1 && f.kinds[0] === ExtendedKind.PUBLICATION)).toBe(
true
)
const dTagFilters = buildLibraryPublicationRelaySearchFiltersForAxis('d-tag', {
query: 'Village Life in China'
})
expect(dTagFilters).toHaveLength(1)
expect(dTagFilters[0].kinds).toEqual([ExtendedKind.PUBLICATION])
expect(dTagFilters[0]['#d']).toContain('village-life-in-china')
expect(dTagFilters[0].search).toBeUndefined()
const titleFilters = buildLibraryPublicationRelaySearchFiltersForAxis('title', {
query: 'Village Life in China'
})
expect(titleFilters).toHaveLength(0)
const authorFilters = buildLibraryPublicationRelaySearchFiltersForAxis('author', {
query: 'Village Life in China'
})
expect(authorFilters).toHaveLength(0)
const merged = buildLibraryPublicationRelaySearchFilters({ query: 'Village Life in China' })
expect(merged).toHaveLength(1)
expect(merged[0]['#d']).toContain('village-life-in-china')
expect(merged.every((f) => f.search == null)).toBe(true)
})
it('filterEventsForPublicationRelaySearchAxis keeps axis-specific kind-30040 matches', () => {
const root = indexEvent('jane-eyre', [`30041:${PK}:intro`])
root.tags = [
['d', 'jane-eyre'],
['title', 'Jane Eyre'],
['author', 'Charlotte Brontë'],
['a', `30041:${PK}:intro`]
]
const byDTag = filterEventsForPublicationRelaySearchAxis([root], 'd-tag', 'jane-eyre')
expect(byDTag).toHaveLength(1)
const byTitle = filterEventsForPublicationRelaySearchAxis([root], 'title', 'jane eyre')
expect(byTitle).toHaveLength(1)
const dFilter = filters.find((f) => f['#d'])
expect(dFilter?.['#d']).toContain('village-life-in-china')
const byAuthor = filterEventsForPublicationRelaySearchAxis([root], 'author', 'charlotte brontë')
expect(byAuthor).toHaveLength(1)
const searchFilter = filters.find((f) => f.search === 'Village Life in China')
expect(searchFilter?.kinds).toEqual([ExtendedKind.PUBLICATION])
expect(filterEventsForPublicationRelaySearchAxis([root], 'title', 'charlotte')).toHaveLength(0)
expect(publicationMetadataTagMatchesQuery(root, 'title', 'Jane Eyre')).toBe(true)
})
it('searchLibraryPublications caches results for repeated queries', async () => {

385
src/lib/library-publication-index.ts

@ -8,7 +8,7 @@ import { @@ -8,7 +8,7 @@ import {
import { normalizeToDTag, parseAdvancedSearch } from '@/lib/search-parser'
import logger from '@/lib/logger'
import { extractNip32LabelValues, isBooklistNip32Label } from '@/lib/nip32-label'
import { queryIndexRelay, queryIndexRelayForLibrary, queryIndexRelayPublicationSearch } from '@/lib/index-relay-http'
import { queryIndexRelay, queryIndexRelayForLibrary, queryIndexRelayPublicationMetadataSearch } from '@/lib/index-relay-http'
import {
buildIndexByAddress,
buildStructuralPublicationIndexMap,
@ -63,6 +63,8 @@ export const LIBRARY_RECENT_FALLBACK_LIMIT = LIBRARY_PAGE_SIZE @@ -63,6 +63,8 @@ export const LIBRARY_RECENT_FALLBACK_LIMIT = LIBRARY_PAGE_SIZE
const LIBRARY_SEARCH_READING_CACHE_LIMIT = 200
export const LIBRARY_RELAY_SEARCH_LIMIT = 100
const LIBRARY_RELAY_SEARCH_TIMEOUT_MS = 28_000
/** Max paginated HTTP pages when title/author metadata API is unavailable (Mercury v0.2.0). */
const LIBRARY_RELAY_SEARCH_SCAN_MAX_PAGES = 80
/** NIP-51 pin list (kind 10001). */
const PIN_LIST_KIND = 10001
/** Per-relay WS page fetch — one relay at a time avoids multi-relay onclose resolving after ~1s. */
@ -1727,6 +1729,15 @@ function normalizePublicationDTag(term: string): string { @@ -1727,6 +1729,15 @@ function normalizePublicationDTag(term: string): string {
.replace(/^-|-$/g, '')
}
/** Relay search axis for kind-30040 publication indexes. */
export type LibraryPublicationRelaySearchAxis = 'd-tag' | 'title' | 'author'
export const LIBRARY_PUBLICATION_RELAY_SEARCH_AXES: LibraryPublicationRelaySearchAxis[] = [
'd-tag',
'title',
'author'
]
/** d-tag filter values: hyphenated slug variants for relay `#d` REQ. */
export function publicationQueryDTagVariants(query: string): string[] {
const raw = query.trim()
@ -1742,14 +1753,99 @@ export function publicationQueryDTagVariants(query: string): string[] { @@ -1742,14 +1753,99 @@ export function publicationQueryDTagVariants(query: string): string[] {
return [...seen]
}
/**
* OR-merge REQ filters for kind **30040** publication indexes: `#d` slugs plus NIP-50 `search`
* (title, author, summary/description on index relays).
*/
export function buildLibraryPublicationRelaySearchFilters(opts: {
/** Normalized needles for exact publication metadata tag match (d / title / author). */
export function publicationQueryNeedles(query: string): string[] {
const raw = normalizeGeneralSearchQuery(query.trim())
if (!raw) return []
const lower = raw.toLowerCase()
const normalized = lower.replace(/\s+/g, ' ').trim()
const hyphen = lower
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
return [...new Set([lower, normalized, hyphen].filter(Boolean))]
}
function publicationTagValueMatchesNeedles(tagValue: string, needles: string[]): boolean {
const val = tagValue.trim().toLowerCase()
const valSpaced = val.replace(/-/g, ' ').replace(/\s+/g, ' ').trim()
for (const needle of needles) {
if (val === needle) return true
const needleSpaced = needle.replace(/-/g, ' ').replace(/\s+/g, ' ').trim()
if (valSpaced === needleSpaced) return true
}
return false
}
export function publicationMetadataTagMatchesQuery(
event: Event,
tagName: 'd' | 'title' | 'author',
query: string
limit?: number
}): Filter[] {
): boolean {
const needles = publicationQueryNeedles(query)
if (needles.length === 0) return false
for (const tag of event.tags ?? []) {
if ((tag[0] || '').toLowerCase() !== tagName) continue
const value = tag[1]?.trim()
if (value && publicationTagValueMatchesNeedles(value, needles)) return true
}
return false
}
function publicationRelaySearchSourceTerms(query: string): string[] {
const raw = query.trim()
if (!raw) return []
const terms = new Set<string>([raw])
const adv = parseAdvancedSearch(raw)
if (adv.title) {
for (const title of Array.isArray(adv.title) ? adv.title : [adv.title]) {
const t = title.trim()
if (t) terms.add(t)
}
}
return [...terms]
}
function publicationRelaySearchTermsForAxis(
axis: LibraryPublicationRelaySearchAxis,
query: string
): string[] {
const raw = query.trim()
if (!raw) return []
const adv = parseAdvancedSearch(raw)
if (axis === 'title' && adv.title) {
const titles = Array.isArray(adv.title) ? adv.title : [adv.title]
const trimmed = titles.map((t) => t.trim()).filter(Boolean)
if (trimmed.length > 0) return [...new Set(trimmed)]
}
if (axis === 'author' && adv.author) {
const authors = Array.isArray(adv.author) ? adv.author : [adv.author]
const trimmed = authors.map((a) => a.trim()).filter(Boolean)
if (trimmed.length > 0) return [...new Set(trimmed)]
}
if (axis === 'd-tag') {
return publicationRelaySearchSourceTerms(raw)
}
return [raw]
}
function addPublicationKindFilter(
out: Filter[],
seen: Set<string>,
filter: Filter
) {
const key = JSON.stringify(filter)
if (seen.has(key)) return
seen.add(key)
out.push(filter)
}
/** One axis of kind-30040 relay discovery: `#d`, metadata title/author (HTTP), or `authors` for npub. */
export function buildLibraryPublicationRelaySearchFiltersForAxis(
axis: LibraryPublicationRelaySearchAxis,
opts: { query: string; limit?: number }
): Filter[] {
const searchRaw = opts.query.trim()
if (!searchRaw) return []
@ -1757,67 +1853,153 @@ export function buildLibraryPublicationRelaySearchFilters(opts: { @@ -1757,67 +1853,153 @@ export function buildLibraryPublicationRelaySearchFilters(opts: {
const kind = ExtendedKind.PUBLICATION
const seen = new Set<string>()
const out: Filter[] = []
const add = (filter: Filter) => {
const key = JSON.stringify(filter)
if (seen.has(key)) return
seen.add(key)
out.push(filter)
if (axis === 'author') {
const npub = tryNpubFromQuery(searchRaw)
if (npub) {
addPublicationKindFilter(out, seen, { kinds: [kind], authors: [npub], limit })
return out
}
return out
}
const npub = tryNpubFromQuery(searchRaw)
if (npub) {
add({ kinds: [kind], authors: [npub], limit })
if (axis === 'title') {
return out
}
const dTags = publicationQueryDTagVariants(searchRaw)
if (dTags.length > 0) {
add({ kinds: [kind], '#d': dTags, limit })
if (axis === 'd-tag') {
const dTags = new Set<string>()
for (const term of publicationRelaySearchTermsForAxis('d-tag', searchRaw)) {
for (const d of publicationQueryDTagVariants(term)) dTags.add(d)
}
if (dTags.size === 0) return []
addPublicationKindFilter(out, seen, { kinds: [kind], '#d': [...dTags], limit })
return out
}
const searchNorm = normalizeGeneralSearchQuery(searchRaw)
add({ kinds: [kind], search: searchRaw, limit })
if (searchNorm !== searchRaw) {
add({ kinds: [kind], search: searchNorm, limit })
return out
}
/**
* REQ filters for kind **30040** publication indexes, split by axis (d-tag, title, author).
* Title and author text use HTTP metadata search (not NIP-50). Only `#d` and pubkey `authors` use NIP-01 filters.
*/
export function buildLibraryPublicationRelaySearchFilters(opts: {
query: string
limit?: number
}): Filter[] {
const seen = new Set<string>()
const out: Filter[] = []
for (const axis of LIBRARY_PUBLICATION_RELAY_SEARCH_AXES) {
for (const filter of buildLibraryPublicationRelaySearchFiltersForAxis(axis, opts)) {
const key = JSON.stringify(filter)
if (seen.has(key)) continue
seen.add(key)
out.push(filter)
}
}
return out
}
export function filterEventsForPublicationRelaySearchAxis(
events: Event[],
axis: LibraryPublicationRelaySearchAxis,
query: string
): Event[] {
const terms = publicationRelaySearchTermsForAxis(axis, query)
if (terms.length === 0) return []
return events.filter((event) => {
if (event.kind !== ExtendedKind.PUBLICATION) return false
if (axis === 'author') {
const npub = tryNpubFromQuery(query.trim())
if (npub && event.pubkey.toLowerCase() === npub) return true
}
const tagName = axis === 'd-tag' ? 'd' : axis
return terms.some((term) => publicationMetadataTagMatchesQuery(event, tagName, term))
})
}
async function scanHttpIndexRelayForPublicationAxis(
httpRelay: string,
axis: LibraryPublicationRelaySearchAxis,
term: string
): Promise<Event[]> {
const filter: Filter = { kinds: [ExtendedKind.PUBLICATION], limit: INDEX_HTTP_PAGE_LIMIT }
const matched: Event[] = []
const seen = new Set<string>()
const collect = (batch: Event[]) => {
for (const ev of filterEventsForPublicationRelaySearchAxis(batch, axis, term)) {
if (!seen.has(ev.id)) {
seen.add(ev.id)
matched.push(ev)
}
}
}
const adv = parseAdvancedSearch(searchRaw)
const titleValues = adv.title
? Array.isArray(adv.title)
? adv.title
: [adv.title]
: []
for (const title of titleValues) {
const t = title.trim()
if (!t) continue
add({ kinds: [kind], search: t, limit })
const titleDTags = publicationQueryDTagVariants(t)
if (titleDTags.length > 0) {
add({ kinds: [kind], '#d': titleDTags, limit })
let firstPage: Event[]
try {
firstPage = (await queryIndexRelayForLibrary(httpRelay, filter)).events as Event[]
} catch (e) {
if (import.meta.env.DEV) {
logger.warn('[Library] HTTP publication scan first page failed', {
relay: httpRelay,
axis,
message: e instanceof Error ? e.message : String(e)
})
}
return []
}
const authorValues = adv.author
? Array.isArray(adv.author)
? adv.author
: [adv.author]
: []
for (const author of authorValues) {
const a = author.trim()
if (a) add({ kinds: [kind], search: a, limit })
collect(firstPage)
if (matched.length >= LIBRARY_RELAY_SEARCH_LIMIT || firstPage.length === 0) {
return matched.slice(0, LIBRARY_RELAY_SEARCH_LIMIT)
}
const descriptionValues = adv.description
? Array.isArray(adv.description)
? adv.description
: [adv.description]
: []
for (const description of descriptionValues) {
const d = description.trim()
if (d) add({ kinds: [kind], search: d, limit })
let until = oldestCreatedAt(firstPage) - 1
for (let page = 1; page < LIBRARY_RELAY_SEARCH_SCAN_MAX_PAGES; page++) {
if (until < 0) break
let batch: Event[] = []
let apiRowCount = 0
try {
const pageResult = await queryIndexRelayForLibrary(httpRelay, { ...filter, until })
batch = pageResult.events as Event[]
apiRowCount = pageResult.apiRowCount
} catch (e) {
if (import.meta.env.DEV) {
logger.warn('[Library] HTTP publication scan page failed', {
relay: httpRelay,
axis,
page,
message: e instanceof Error ? e.message : String(e)
})
}
break
}
if (apiRowCount === 0) break
collect(batch)
if (matched.length >= LIBRARY_RELAY_SEARCH_LIMIT) break
if (apiRowCount < INDEX_HTTP_PAGE_LIMIT) break
const oldest = oldestCreatedAt(batch)
if (oldest === Number.MAX_SAFE_INTEGER) break
until = oldest - 1
}
return out
return matched.slice(0, LIBRARY_RELAY_SEARCH_LIMIT)
}
async function searchHttpIndexRelayPublicationAxis(
httpRelay: string,
axis: LibraryPublicationRelaySearchAxis,
term: string
): Promise<Event[]> {
const meta = await queryIndexRelayPublicationMetadataSearch(httpRelay, term, {
limit: LIBRARY_RELAY_SEARCH_LIMIT
})
const fromApi = filterEventsForPublicationRelaySearchAxis(meta.events as Event[], axis, term)
if (fromApi.length > 0) return fromApi
return scanHttpIndexRelayForPublicationAxis(httpRelay, axis, term)
}
/** Query document relays for kind-30040 indexes matching {@link buildLibraryPublicationRelaySearchFilters}. */
@ -1852,43 +2034,35 @@ export async function searchLibraryPublicationsOnRelays( @@ -1852,43 +2034,35 @@ export async function searchLibraryPublicationsOnRelays(
}
}
const filters = buildLibraryPublicationRelaySearchFilters({ query: q })
if (filters.length === 0) {
return { events: [], entries: [], mergedIndexEvents: context.indexEvents ?? [], fromCache: false }
}
const indexRelays = libraryIndexRelayUrls(relayUrls)
const { wsRelays, httpRelays } = splitWsAndHttpRelays(indexRelays)
const batches: Promise<Event[]>[] = []
let filterCount = 0
if (wsRelays.length > 0) {
batches.push(
queryService
.fetchEvents(wsRelays, filters, {
globalTimeout: LIBRARY_RELAY_SEARCH_TIMEOUT_MS,
eoseTimeout: 8_000,
firstRelayResultGraceMs: false
})
.catch((e) => {
if (import.meta.env.DEV) {
logger.warn('[Library] WS publication search failed', {
message: e instanceof Error ? e.message : String(e)
})
}
return [] as Event[]
})
)
}
for (const axis of LIBRARY_PUBLICATION_RELAY_SEARCH_AXES) {
const npubQuery = tryNpubFromQuery(q)
if (npubQuery && axis !== 'author') continue
for (const httpRelay of httpRelays) {
for (const filter of filters) {
const axisFilters = buildLibraryPublicationRelaySearchFiltersForAxis(axis, { query: q })
const hasNip01Filters = axisFilters.length > 0
const hasMetadataSearch = axis === 'title' || (axis === 'author' && !npubQuery)
if (!hasNip01Filters && !hasMetadataSearch) continue
filterCount += axisFilters.length
if (wsRelays.length > 0 && hasNip01Filters) {
batches.push(
queryIndexRelayPublicationSearch(httpRelay, filter)
.then((page) => page.events as Event[])
queryService
.fetchEvents(wsRelays, axisFilters, {
globalTimeout: LIBRARY_RELAY_SEARCH_TIMEOUT_MS,
eoseTimeout: 8_000,
firstRelayResultGraceMs: false
})
.then((events) => filterEventsForPublicationRelaySearchAxis(events, axis, q))
.catch((e) => {
if (import.meta.env.DEV) {
logger.warn('[Library] HTTP publication search failed', {
relay: httpRelay,
logger.warn('[Library] WS publication search failed', {
axis,
message: e instanceof Error ? e.message : String(e)
})
}
@ -1896,6 +2070,49 @@ export async function searchLibraryPublicationsOnRelays( @@ -1896,6 +2070,49 @@ export async function searchLibraryPublicationsOnRelays(
})
)
}
for (const httpRelay of httpRelays) {
if (hasNip01Filters) {
for (const filter of axisFilters) {
batches.push(
queryIndexRelayForLibrary(httpRelay, filter)
.then((page) => filterEventsForPublicationRelaySearchAxis(page.events as Event[], axis, q))
.catch((e) => {
if (import.meta.env.DEV) {
logger.warn('[Library] HTTP publication filter search failed', {
relay: httpRelay,
axis,
message: e instanceof Error ? e.message : String(e)
})
}
return [] as Event[]
})
)
}
}
if (!hasMetadataSearch) continue
for (const term of publicationRelaySearchTermsForAxis(axis, q)) {
filterCount += 1
batches.push(
searchHttpIndexRelayPublicationAxis(httpRelay, axis, term).catch((e) => {
if (import.meta.env.DEV) {
logger.warn('[Library] HTTP publication metadata search failed', {
relay: httpRelay,
axis,
message: e instanceof Error ? e.message : String(e)
})
}
return [] as Event[]
})
)
}
}
}
if (batches.length === 0) {
return { events: [], entries: [], mergedIndexEvents: context.indexEvents ?? [], fromCache: false }
}
const settled = await Promise.all(batches)
@ -1926,7 +2143,9 @@ export async function searchLibraryPublicationsOnRelays( @@ -1926,7 +2143,9 @@ export async function searchLibraryPublicationsOnRelays(
if (import.meta.env.DEV) {
logger.info('[Library] relay search done', {
filters: filters.length,
axes: LIBRARY_PUBLICATION_RELAY_SEARCH_AXES.length,
filters: filterCount,
batches: batches.length,
network: networkEvents.length,
valid: valid.length,
roots: roots.length

7
src/lib/metadata-policy-curated-relays.test.ts

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { DOCUMENT_RELAY_URLS, FAST_READ_RELAY_URLS, PROFILE_RELAY_URLS } from '@/constants'
import { DOCUMENT_RELAY_URLS, PROFILE_RELAY_URLS } from '@/constants'
import { describe, expect, it } from 'vitest'
import {
isMetadataPolicyActiveReadGrantRelay,
@ -15,13 +15,14 @@ describe('metadata-policy-curated-relays', () => { @@ -15,13 +15,14 @@ describe('metadata-policy-curated-relays', () => {
it('operation scope excludes FAST_READ widening', () => {
expect(isMetadataPolicyOperationScopedRelay(DOCUMENT_RELAY_URLS[0]!)).toBe(true)
expect(isMetadataPolicyOperationScopedRelay(PROFILE_RELAY_URLS[0]!)).toBe(true)
expect(isMetadataPolicyOperationScopedRelay(FAST_READ_RELAY_URLS[0]!)).toBe(false)
expect(isMetadataPolicyOperationScopedRelay('wss://nostr21.com/')).toBe(false)
expect(isMetadataPolicyOperationScopedRelay('wss://nostr.wirednet.jp/')).toBe(false)
})
it('active read grant includes search and discovery stacks', () => {
expect(isMetadataPolicyActiveReadGrantRelay('wss://search.nos.today/')).toBe(true)
expect(isMetadataPolicyActiveReadGrantRelay(FAST_READ_RELAY_URLS[0]!)).toBe(false)
expect(isMetadataPolicyActiveReadGrantRelay('wss://nostr21.com/')).toBe(true)
expect(isMetadataPolicyActiveReadGrantRelay('wss://relay.primal.net/')).toBe(false)
expect(isMetadataPolicyActiveReadGrantRelay('wss://nostr.wirednet.jp/')).toBe(false)
})
})

1
src/lib/new-user-template-broadcast.ts

@ -65,7 +65,6 @@ function prioritizeNewUserTemplateRelays(urls: string[]): string[] { @@ -65,7 +65,6 @@ function prioritizeNewUserTemplateRelays(urls: string[]): string[] {
'wss://profiles.nostr1.com',
'wss://nos.lol',
'wss://relay.primal.net',
'wss://relay.damus.io',
'wss://thecitadel.nostr1.com'
]
const byKey = new Map(urls.map((u) => [templateRelayKey(u), u]))

3
src/lib/new-user-template.ts

@ -14,6 +14,7 @@ import { @@ -14,6 +14,7 @@ import {
createProfileDraftEvent,
createRelayListDraftEvent
} from '@/lib/draft-event'
import { dedupeNormalizeRelayUrlsOrdered } from '@/lib/relay-url-priority'
import { TDraftEvent, TMailboxRelay } from '@/types'
export const NEW_USER_HTTP_RELAY_URL = 'https://mercury-relay.imwald.eu/'
@ -75,7 +76,7 @@ export function buildNewUserProfileDraft(pubkey: string): TDraftEvent { @@ -75,7 +76,7 @@ export function buildNewUserProfileDraft(pubkey: string): TDraftEvent {
export function buildNewUserFavoriteRelaysDraft(): TDraftEvent {
return createFavoriteRelaysDraftEvent(
[...DEFAULT_FAVORITE_RELAYS, NEW_USER_TRENDING_RELAY_URL],
dedupeNormalizeRelayUrlsOrdered([...DEFAULT_FAVORITE_RELAYS, NEW_USER_TRENDING_RELAY_URL]),
[]
)
}

2
src/services/nip89.service.ts

@ -237,7 +237,7 @@ class Nip89Service { @@ -237,7 +237,7 @@ class Nip89Service {
desktop: 'imwald://note/bech32'
},
relays: [
'wss://relay.damus.io',
'wss://thecitadel.nostr1.com',
'wss://relay.snort.social',
'wss://nos.lol'
]

Loading…
Cancel
Save