mirror of
https://github.com/dergigi/boris.git
synced 2025-12-24 01:54:19 +01:00
perf(relays): local-first queries with short timeouts; fallback to remote if needed
This commit is contained in:
@@ -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')
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user