perf(relays): local-first queries with short timeouts; fallback to remote if needed

This commit is contained in:
Gigi
2025-10-12 22:06:49 +02:00
parent 86de98e644
commit 5513fc9850
6 changed files with 255 additions and 66 deletions

View File

@@ -1,10 +1,11 @@
import { RelayPool, completeOnEose } from 'applesauce-relay' import { RelayPool, completeOnEose } from 'applesauce-relay'
import { lastValueFrom, takeUntil, timer, toArray } from 'rxjs' import { lastValueFrom, race, takeUntil, timer, toArray } from 'rxjs'
import { nip19 } from 'nostr-tools' import { nip19 } from 'nostr-tools'
import { AddressPointer } from 'nostr-tools/nip19' import { AddressPointer } from 'nostr-tools/nip19'
import { NostrEvent } from 'nostr-tools' import { NostrEvent } from 'nostr-tools'
import { Helpers } from 'applesauce-core' import { Helpers } from 'applesauce-core'
import { RELAYS } from '../config/relays' import { RELAYS } from '../config/relays'
import { prioritizeLocalRelays, partitionRelays } from '../utils/helpers'
import { UserSettings } from './settingsService' import { UserSettings } from './settingsService'
import { rebroadcastEvents } from './rebroadcastService' import { rebroadcastEvents } from './rebroadcastService'
@@ -98,9 +99,11 @@ export async function fetchArticleByNaddr(
const pointer = decoded.data as AddressPointer const pointer = decoded.data as AddressPointer
// Define relays to query - prefer relays from naddr, fallback to configured relays (including local) // Define relays to query - prefer relays from naddr, fallback to configured relays (including local)
const relays = pointer.relays && pointer.relays.length > 0 const baseRelays = pointer.relays && pointer.relays.length > 0
? pointer.relays ? pointer.relays
: RELAYS : RELAYS
const orderedRelays = prioritizeLocalRelays(baseRelays)
const { local: localRelays, remote: remoteRelays } = partitionRelays(orderedRelays)
// Fetch the article event // Fetch the article event
const filter = { const filter = {
@@ -109,12 +112,28 @@ export async function fetchArticleByNaddr(
'#d': [pointer.identifier] '#d': [pointer.identifier]
} }
// Use applesauce relay pool pattern // Local-first: try local relays quickly, then fallback to remote if no result
const events = await lastValueFrom( let events = [] as NostrEvent[]
relayPool if (localRelays.length > 0) {
.req(relays, filter) try {
.pipe(completeOnEose(), takeUntil(timer(10000)), toArray()) events = await lastValueFrom(
) relayPool
.req(localRelays, filter)
.pipe(completeOnEose(), takeUntil(timer(1200)), toArray())
)
} catch {
events = []
}
}
if (events.length === 0) {
// Fallback: query all relays, but still time-box
events = await lastValueFrom(
relayPool
.req(orderedRelays, filter)
.pipe(completeOnEose(), takeUntil(timer(6000)), toArray())
)
}
if (events.length === 0) { if (events.length === 0) {
throw new Error('Article not found') throw new Error('Article not found')

View File

@@ -16,6 +16,7 @@ import { Bookmark } from '../types/bookmarks'
import { collectBookmarksFromEvents } from './bookmarkProcessing.ts' import { collectBookmarksFromEvents } from './bookmarkProcessing.ts'
import { UserSettings } from './settingsService' import { UserSettings } from './settingsService'
import { rebroadcastEvents } from './rebroadcastService' import { rebroadcastEvents } from './rebroadcastService'
import { prioritizeLocalRelays } from '../utils/helpers'
@@ -31,14 +32,30 @@ export const fetchBookmarks = async (
throw new Error('Invalid account object provided') throw new Error('Invalid account object provided')
} }
// Get relay URLs from the pool // Get relay URLs from the pool
const relayUrls = Array.from(relayPool.relays.values()).map(relay => relay.url) const relayUrls = prioritizeLocalRelays(Array.from(relayPool.relays.values()).map(relay => relay.url))
// Fetch bookmark events - NIP-51 standards, legacy formats, and web bookmarks (NIP-B0) // Fetch bookmark events - NIP-51 standards, legacy formats, and web bookmarks (NIP-B0)
console.log('🔍 Fetching bookmark events from relays:', relayUrls) console.log('🔍 Fetching bookmark events from relays:', relayUrls)
const rawEvents = await lastValueFrom( // Try local-first quickly, then full set fallback
relayPool let rawEvents = [] as NostrEvent[]
.req(relayUrls, { kinds: [10003, 30003, 30001, 39701], authors: [activeAccount.pubkey] }) const localRelays = relayUrls.filter(url => url.includes('localhost') || url.includes('127.0.0.1'))
.pipe(completeOnEose(), takeUntil(timer(20000)), toArray()) if (localRelays.length > 0) {
) try {
rawEvents = await lastValueFrom(
relayPool
.req(localRelays, { kinds: [10003, 30003, 30001, 39701], authors: [activeAccount.pubkey] })
.pipe(completeOnEose(), takeUntil(timer(1200)), toArray())
)
} catch {
rawEvents = []
}
}
if (rawEvents.length === 0) {
rawEvents = await lastValueFrom(
relayPool
.req(relayUrls, { kinds: [10003, 30003, 30001, 39701], authors: [activeAccount.pubkey] })
.pipe(completeOnEose(), takeUntil(timer(6000)), toArray())
)
}
console.log('📊 Raw events fetched:', rawEvents.length, 'events') console.log('📊 Raw events fetched:', rawEvents.length, 'events')
// Rebroadcast bookmark events to local/all relays based on settings // Rebroadcast bookmark events to local/all relays based on settings
@@ -103,7 +120,9 @@ export const fetchBookmarks = async (
if (noteIds.length > 0) { if (noteIds.length > 0) {
try { try {
const events = await lastValueFrom( const events = await lastValueFrom(
relayPool.req(relayUrls, { ids: noteIds }).pipe(completeOnEose(), takeUntil(timer(10000)), toArray()) relayPool
.req(relayUrls, { ids: noteIds })
.pipe(completeOnEose(), takeUntil(timer(4000)), toArray())
) )
idToEvent = new Map(events.map((e: NostrEvent) => [e.id, e])) idToEvent = new Map(events.map((e: NostrEvent) => [e.id, e]))
} catch (error) { } catch (error) {

View File

@@ -1,5 +1,6 @@
import { RelayPool, completeOnEose } from 'applesauce-relay' import { RelayPool, completeOnEose } from 'applesauce-relay'
import { lastValueFrom, takeUntil, timer, toArray } from 'rxjs' import { lastValueFrom, takeUntil, timer, toArray } from 'rxjs'
import { prioritizeLocalRelays } from '../utils/helpers'
/** /**
* Fetches the contact list (follows) for a specific user * Fetches the contact list (follows) for a specific user
@@ -12,15 +13,31 @@ export const fetchContacts = async (
pubkey: string pubkey: string
): Promise<Set<string>> => { ): Promise<Set<string>> => {
try { try {
const relayUrls = Array.from(relayPool.relays.values()).map(relay => relay.url) const relayUrls = prioritizeLocalRelays(Array.from(relayPool.relays.values()).map(relay => relay.url))
console.log('🔍 Fetching contacts (kind 3) for user:', pubkey) console.log('🔍 Fetching contacts (kind 3) for user:', pubkey)
const events = await lastValueFrom( // Local-first quick attempt
relayPool const localRelays = relayUrls.filter(url => url.includes('localhost') || url.includes('127.0.0.1'))
.req(relayUrls, { kinds: [3], authors: [pubkey] }) let events = [] as any[]
.pipe(completeOnEose(), takeUntil(timer(10000)), toArray()) if (localRelays.length > 0) {
) try {
events = await lastValueFrom(
relayPool
.req(localRelays, { kinds: [3], authors: [pubkey] })
.pipe(completeOnEose(), takeUntil(timer(1200)), toArray())
)
} catch {
events = []
}
}
if (events.length === 0) {
events = await lastValueFrom(
relayPool
.req(relayUrls, { kinds: [3], authors: [pubkey] })
.pipe(completeOnEose(), takeUntil(timer(6000)), toArray())
)
}
console.log('📊 Contact events fetched:', events.length) console.log('📊 Contact events fetched:', events.length)

View File

@@ -1,5 +1,6 @@
import { RelayPool, completeOnEose } from 'applesauce-relay' import { RelayPool, completeOnEose } from 'applesauce-relay'
import { lastValueFrom, takeUntil, timer, toArray } from 'rxjs' import { lastValueFrom, takeUntil, timer, toArray } from 'rxjs'
import { prioritizeLocalRelays } from '../utils/helpers'
import { NostrEvent } from 'nostr-tools' import { NostrEvent } from 'nostr-tools'
import { Helpers } from 'applesauce-core' import { Helpers } from 'applesauce-core'
@@ -34,15 +35,36 @@ export const fetchBlogPostsFromAuthors = async (
console.log('📚 Fetching blog posts (kind 30023) from', pubkeys.length, 'authors') console.log('📚 Fetching blog posts (kind 30023) from', pubkeys.length, 'authors')
const events = await lastValueFrom( const prioritized = prioritizeLocalRelays(relayUrls)
relayPool const localRelays = prioritized.filter(url => url.includes('localhost') || url.includes('127.0.0.1'))
.req(relayUrls, {
kinds: [30023], let events = [] as NostrEvent[]
authors: pubkeys, if (localRelays.length > 0) {
limit: 100 // Fetch up to 100 recent posts try {
}) events = await lastValueFrom(
.pipe(completeOnEose(), takeUntil(timer(15000)), toArray()) relayPool
) .req(localRelays, {
kinds: [30023],
authors: pubkeys,
limit: 100
})
.pipe(completeOnEose(), takeUntil(timer(1200)), toArray())
)
} catch {
events = []
}
}
if (events.length === 0) {
events = await lastValueFrom(
relayPool
.req(prioritized, {
kinds: [30023],
authors: pubkeys,
limit: 100
})
.pipe(completeOnEose(), takeUntil(timer(6000)), toArray())
)
}
console.log('📊 Blog post events fetched:', events.length) console.log('📊 Blog post events fetched:', events.length)

View File

@@ -3,6 +3,7 @@ import { lastValueFrom, takeUntil, timer, tap, toArray } from 'rxjs'
import { NostrEvent } from 'nostr-tools' import { NostrEvent } from 'nostr-tools'
import { Highlight } from '../types/highlights' import { Highlight } from '../types/highlights'
import { RELAYS } from '../config/relays' import { RELAYS } from '../config/relays'
import { prioritizeLocalRelays, partitionRelays } from '../utils/helpers'
import { eventToHighlight, dedupeHighlights, sortHighlights } from './highlightEventProcessor' import { eventToHighlight, dedupeHighlights, sortHighlights } from './highlightEventProcessor'
import { UserSettings } from './settingsService' import { UserSettings } from './settingsService'
import { rebroadcastEvents } from './rebroadcastService' import { rebroadcastEvents } from './rebroadcastService'
@@ -34,32 +35,39 @@ export const fetchHighlightsForArticle = async (
return eventToHighlight(event) return eventToHighlight(event)
} }
// Local-first relay ordering
const orderedRelays = prioritizeLocalRelays(RELAYS)
const { local: localRelays, remote: remoteRelays } = partitionRelays(orderedRelays)
// Query for highlights that reference this article via the 'a' tag // Query for highlights that reference this article via the 'a' tag
const aTagEvents = await lastValueFrom( let aTagEvents: NostrEvent[] = []
relayPool if (localRelays.length > 0) {
.req(RELAYS, { kinds: [9802], '#a': [articleCoordinate] }) try {
.pipe( aTagEvents = await lastValueFrom(
onlyEvents(), relayPool
tap((event: NostrEvent) => { .req(localRelays, { kinds: [9802], '#a': [articleCoordinate] })
const highlight = processEvent(event) .pipe(
if (highlight && onHighlight) { onlyEvents(),
onHighlight(highlight) tap((event: NostrEvent) => {
} const highlight = processEvent(event)
}), if (highlight && onHighlight) {
completeOnEose(), onHighlight(highlight)
takeUntil(timer(10000)), }
toArray() }),
completeOnEose(),
takeUntil(timer(1200)),
toArray()
)
) )
) } catch {
aTagEvents = []
}
}
console.log('📊 Highlights via a-tag:', aTagEvents.length) if (aTagEvents.length === 0) {
aTagEvents = await lastValueFrom(
// If we have an event ID, also query for highlights that reference via the 'e' tag
let eTagEvents: NostrEvent[] = []
if (eventId) {
eTagEvents = await lastValueFrom(
relayPool relayPool
.req(RELAYS, { kinds: [9802], '#e': [eventId] }) .req(orderedRelays, { kinds: [9802], '#a': [articleCoordinate] })
.pipe( .pipe(
onlyEvents(), onlyEvents(),
tap((event: NostrEvent) => { tap((event: NostrEvent) => {
@@ -69,10 +77,59 @@ export const fetchHighlightsForArticle = async (
} }
}), }),
completeOnEose(), completeOnEose(),
takeUntil(timer(10000)), takeUntil(timer(6000)),
toArray() toArray()
) )
) )
}
console.log('📊 Highlights via a-tag:', aTagEvents.length)
// If we have an event ID, also query for highlights that reference via the 'e' tag
let eTagEvents: NostrEvent[] = []
if (eventId) {
// e-tag query local-first as well
if (localRelays.length > 0) {
try {
eTagEvents = await lastValueFrom(
relayPool
.req(localRelays, { kinds: [9802], '#e': [eventId] })
.pipe(
onlyEvents(),
tap((event: NostrEvent) => {
const highlight = processEvent(event)
if (highlight && onHighlight) {
onHighlight(highlight)
}
}),
completeOnEose(),
takeUntil(timer(1200)),
toArray()
)
)
} catch {
eTagEvents = []
}
}
if (eTagEvents.length === 0) {
eTagEvents = await lastValueFrom(
relayPool
.req(orderedRelays, { kinds: [9802], '#e': [eventId] })
.pipe(
onlyEvents(),
tap((event: NostrEvent) => {
const highlight = processEvent(event)
if (highlight && onHighlight) {
onHighlight(highlight)
}
}),
completeOnEose(),
takeUntil(timer(6000)),
toArray()
)
)
}
console.log('📊 Highlights via e-tag:', eTagEvents.length) console.log('📊 Highlights via e-tag:', eTagEvents.length)
} }
@@ -118,19 +175,43 @@ export const fetchHighlightsForUrl = async (
console.log('🔍 Fetching highlights (kind 9802) for URL:', url) console.log('🔍 Fetching highlights (kind 9802) for URL:', url)
const seenIds = new Set<string>() const seenIds = new Set<string>()
const rawEvents = await lastValueFrom( const orderedRelaysUrl = prioritizeLocalRelays(RELAYS)
relayPool const { local: localRelaysUrl } = partitionRelays(orderedRelaysUrl)
.req(RELAYS, { kinds: [9802], '#r': [url] }) let rawEvents: NostrEvent[] = []
.pipe( if (localRelaysUrl.length > 0) {
onlyEvents(), try {
tap((event: NostrEvent) => { rawEvents = await lastValueFrom(
seenIds.add(event.id) relayPool
}), .req(localRelaysUrl, { kinds: [9802], '#r': [url] })
completeOnEose(), .pipe(
takeUntil(timer(10000)), onlyEvents(),
toArray() tap((event: NostrEvent) => {
seenIds.add(event.id)
}),
completeOnEose(),
takeUntil(timer(1200)),
toArray()
)
) )
) } catch {
rawEvents = []
}
}
if (rawEvents.length === 0) {
rawEvents = await lastValueFrom(
relayPool
.req(orderedRelaysUrl, { kinds: [9802], '#r': [url] })
.pipe(
onlyEvents(),
tap((event: NostrEvent) => {
seenIds.add(event.id)
}),
completeOnEose(),
takeUntil(timer(6000)),
toArray()
)
)
}
console.log('📊 Highlights for URL:', rawEvents.length) console.log('📊 Highlights for URL:', rawEvents.length)

View File

@@ -63,3 +63,34 @@ export const hasRemoteRelay = (relayUrls: string[]): boolean => {
return relayUrls.some(url => !isLocalRelay(url)) return relayUrls.some(url => !isLocalRelay(url))
} }
/**
* Splits relay URLs into local and remote groups
*/
export const partitionRelays = (
relayUrls: string[]
): { local: string[]; remote: string[] } => {
const local: string[] = []
const remote: string[] = []
for (const url of relayUrls) {
if (isLocalRelay(url)) local.push(url)
else remote.push(url)
}
return { local, remote }
}
/**
* Returns relays ordered with local first while keeping uniqueness
*/
export const prioritizeLocalRelays = (relayUrls: string[]): string[] => {
const { local, remote } = partitionRelays(relayUrls)
const seen = new Set<string>()
const out: string[] = []
for (const url of [...local, ...remote]) {
if (!seen.has(url)) {
seen.add(url)
out.push(url)
}
}
return out
}