diff --git a/src/services/articleService.ts b/src/services/articleService.ts index d1d8d320..a2b94f14 100644 --- a/src/services/articleService.ts +++ b/src/services/articleService.ts @@ -1,10 +1,11 @@ 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 { AddressPointer } from 'nostr-tools/nip19' import { NostrEvent } from 'nostr-tools' import { Helpers } from 'applesauce-core' import { RELAYS } from '../config/relays' +import { prioritizeLocalRelays, partitionRelays } from '../utils/helpers' import { UserSettings } from './settingsService' import { rebroadcastEvents } from './rebroadcastService' @@ -98,9 +99,11 @@ export async function fetchArticleByNaddr( const pointer = decoded.data as AddressPointer // 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 : RELAYS + const orderedRelays = prioritizeLocalRelays(baseRelays) + const { local: localRelays, remote: remoteRelays } = partitionRelays(orderedRelays) // Fetch the article event const filter = { @@ -109,12 +112,28 @@ export async function fetchArticleByNaddr( '#d': [pointer.identifier] } - // Use applesauce relay pool pattern - const events = await lastValueFrom( - relayPool - .req(relays, filter) - .pipe(completeOnEose(), takeUntil(timer(10000)), toArray()) - ) + // Local-first: try local relays quickly, then fallback to remote if no result + let events = [] as NostrEvent[] + if (localRelays.length > 0) { + try { + 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) { throw new Error('Article not found') diff --git a/src/services/bookmarkService.ts b/src/services/bookmarkService.ts index e147a59b..84198162 100644 --- a/src/services/bookmarkService.ts +++ b/src/services/bookmarkService.ts @@ -16,6 +16,7 @@ import { Bookmark } from '../types/bookmarks' import { collectBookmarksFromEvents } from './bookmarkProcessing.ts' import { UserSettings } from './settingsService' import { rebroadcastEvents } from './rebroadcastService' +import { prioritizeLocalRelays } from '../utils/helpers' @@ -31,14 +32,30 @@ export const fetchBookmarks = async ( throw new Error('Invalid account object provided') } // 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) console.log('🔍 Fetching bookmark events from relays:', relayUrls) - const rawEvents = await lastValueFrom( - relayPool - .req(relayUrls, { kinds: [10003, 30003, 30001, 39701], authors: [activeAccount.pubkey] }) - .pipe(completeOnEose(), takeUntil(timer(20000)), toArray()) - ) + // Try local-first quickly, then full set fallback + let rawEvents = [] as NostrEvent[] + const localRelays = relayUrls.filter(url => url.includes('localhost') || url.includes('127.0.0.1')) + 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') // Rebroadcast bookmark events to local/all relays based on settings @@ -103,7 +120,9 @@ export const fetchBookmarks = async ( if (noteIds.length > 0) { try { 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])) } catch (error) { diff --git a/src/services/contactService.ts b/src/services/contactService.ts index b7d64c19..dc69b3b2 100644 --- a/src/services/contactService.ts +++ b/src/services/contactService.ts @@ -1,5 +1,6 @@ import { RelayPool, completeOnEose } from 'applesauce-relay' import { lastValueFrom, takeUntil, timer, toArray } from 'rxjs' +import { prioritizeLocalRelays } from '../utils/helpers' /** * Fetches the contact list (follows) for a specific user @@ -12,15 +13,31 @@ export const fetchContacts = async ( pubkey: string ): Promise> => { 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) - const events = await lastValueFrom( - relayPool - .req(relayUrls, { kinds: [3], authors: [pubkey] }) - .pipe(completeOnEose(), takeUntil(timer(10000)), toArray()) - ) + // Local-first quick attempt + const localRelays = relayUrls.filter(url => url.includes('localhost') || url.includes('127.0.0.1')) + let events = [] as any[] + 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) diff --git a/src/services/exploreService.ts b/src/services/exploreService.ts index 38b7d0ce..795a9087 100644 --- a/src/services/exploreService.ts +++ b/src/services/exploreService.ts @@ -1,5 +1,6 @@ import { RelayPool, completeOnEose } from 'applesauce-relay' import { lastValueFrom, takeUntil, timer, toArray } from 'rxjs' +import { prioritizeLocalRelays } from '../utils/helpers' import { NostrEvent } from 'nostr-tools' import { Helpers } from 'applesauce-core' @@ -34,15 +35,36 @@ export const fetchBlogPostsFromAuthors = async ( console.log('📚 Fetching blog posts (kind 30023) from', pubkeys.length, 'authors') - const events = await lastValueFrom( - relayPool - .req(relayUrls, { - kinds: [30023], - authors: pubkeys, - limit: 100 // Fetch up to 100 recent posts - }) - .pipe(completeOnEose(), takeUntil(timer(15000)), toArray()) - ) + const prioritized = prioritizeLocalRelays(relayUrls) + const localRelays = prioritized.filter(url => url.includes('localhost') || url.includes('127.0.0.1')) + + let events = [] as NostrEvent[] + if (localRelays.length > 0) { + try { + events = await lastValueFrom( + 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) diff --git a/src/services/highlightService.ts b/src/services/highlightService.ts index 65f62908..fd07adf8 100644 --- a/src/services/highlightService.ts +++ b/src/services/highlightService.ts @@ -3,6 +3,7 @@ import { lastValueFrom, takeUntil, timer, tap, toArray } from 'rxjs' import { NostrEvent } from 'nostr-tools' import { Highlight } from '../types/highlights' import { RELAYS } from '../config/relays' +import { prioritizeLocalRelays, partitionRelays } from '../utils/helpers' import { eventToHighlight, dedupeHighlights, sortHighlights } from './highlightEventProcessor' import { UserSettings } from './settingsService' import { rebroadcastEvents } from './rebroadcastService' @@ -34,32 +35,39 @@ export const fetchHighlightsForArticle = async ( 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 - const aTagEvents = await lastValueFrom( - relayPool - .req(RELAYS, { kinds: [9802], '#a': [articleCoordinate] }) - .pipe( - onlyEvents(), - tap((event: NostrEvent) => { - const highlight = processEvent(event) - if (highlight && onHighlight) { - onHighlight(highlight) - } - }), - completeOnEose(), - takeUntil(timer(10000)), - toArray() + let aTagEvents: NostrEvent[] = [] + if (localRelays.length > 0) { + try { + aTagEvents = await lastValueFrom( + relayPool + .req(localRelays, { kinds: [9802], '#a': [articleCoordinate] }) + .pipe( + onlyEvents(), + tap((event: NostrEvent) => { + const highlight = processEvent(event) + if (highlight && onHighlight) { + onHighlight(highlight) + } + }), + completeOnEose(), + takeUntil(timer(1200)), + 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) { - eTagEvents = await lastValueFrom( + } catch { + aTagEvents = [] + } + } + + if (aTagEvents.length === 0) { + aTagEvents = await lastValueFrom( relayPool - .req(RELAYS, { kinds: [9802], '#e': [eventId] }) + .req(orderedRelays, { kinds: [9802], '#a': [articleCoordinate] }) .pipe( onlyEvents(), tap((event: NostrEvent) => { @@ -69,10 +77,59 @@ export const fetchHighlightsForArticle = async ( } }), completeOnEose(), - takeUntil(timer(10000)), + takeUntil(timer(6000)), 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) } @@ -118,19 +175,43 @@ export const fetchHighlightsForUrl = async ( console.log('🔍 Fetching highlights (kind 9802) for URL:', url) const seenIds = new Set() - const rawEvents = await lastValueFrom( - relayPool - .req(RELAYS, { kinds: [9802], '#r': [url] }) - .pipe( - onlyEvents(), - tap((event: NostrEvent) => { - seenIds.add(event.id) - }), - completeOnEose(), - takeUntil(timer(10000)), - toArray() + const orderedRelaysUrl = prioritizeLocalRelays(RELAYS) + const { local: localRelaysUrl } = partitionRelays(orderedRelaysUrl) + let rawEvents: NostrEvent[] = [] + if (localRelaysUrl.length > 0) { + try { + rawEvents = await lastValueFrom( + relayPool + .req(localRelaysUrl, { kinds: [9802], '#r': [url] }) + .pipe( + onlyEvents(), + 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) diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index da3c12ef..b162f9d0 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -63,3 +63,34 @@ export const hasRemoteRelay = (relayUrls: string[]): boolean => { 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() + const out: string[] = [] + for (const url of [...local, ...remote]) { + if (!seen.has(url)) { + seen.add(url) + out.push(url) + } + } + return out +} +