|
|
|
|
@ -1,11 +1,80 @@
@@ -1,11 +1,80 @@
|
|
|
|
|
import { FAST_READ_RELAY_URLS } from '@/constants' |
|
|
|
|
import logger from '@/lib/logger' |
|
|
|
|
import { normalizeUrl } from '@/lib/url' |
|
|
|
|
import type { Event as NEvent, Filter } from 'nostr-tools' |
|
|
|
|
import { nip19 } from 'nostr-tools' |
|
|
|
|
import DataLoader from 'dataloader' |
|
|
|
|
import { LRUCache } from 'lru-cache' |
|
|
|
|
import indexedDb from './indexed-db.service' |
|
|
|
|
import type { QueryService } from './client-query.service' |
|
|
|
|
import client from './client.service' |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Build comprehensive relay list: author's outboxes + user's inboxes + relay hints + defaults |
|
|
|
|
*/ |
|
|
|
|
async function buildComprehensiveRelayList( |
|
|
|
|
authorPubkey: string | undefined, |
|
|
|
|
relayHints: string[] = [], |
|
|
|
|
seenRelays: string[] = [] |
|
|
|
|
): Promise<string[]> { |
|
|
|
|
const relayUrls = new Set<string>() |
|
|
|
|
|
|
|
|
|
// 1. Add relay hints (highest priority - these are explicit hints)
|
|
|
|
|
relayHints.forEach(url => { |
|
|
|
|
const normalized = normalizeUrl(url) |
|
|
|
|
if (normalized) relayUrls.add(normalized) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// 2. Add relays where event was seen
|
|
|
|
|
seenRelays.forEach(url => { |
|
|
|
|
const normalized = normalizeUrl(url) |
|
|
|
|
if (normalized) relayUrls.add(normalized) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// 3. Add author's outboxes (write relays) - where they publish
|
|
|
|
|
if (authorPubkey) { |
|
|
|
|
try { |
|
|
|
|
const authorRelayList = await client.fetchRelayList(authorPubkey) |
|
|
|
|
const authorOutboxes = (authorRelayList.write || []).slice(0, 10) // Limit to 10 to avoid too many
|
|
|
|
|
authorOutboxes.forEach(url => { |
|
|
|
|
const normalized = normalizeUrl(url) |
|
|
|
|
if (normalized) relayUrls.add(normalized) |
|
|
|
|
}) |
|
|
|
|
logger.debug('[EventService] Added author outboxes', { |
|
|
|
|
author: authorPubkey.substring(0, 8), |
|
|
|
|
count: authorOutboxes.length |
|
|
|
|
}) |
|
|
|
|
} catch (error) { |
|
|
|
|
logger.debug('[EventService] Failed to fetch author relay list', { error }) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 4. Add logged-in user's inboxes (read relays) - where they receive events
|
|
|
|
|
const userPubkey = client.pubkey |
|
|
|
|
if (userPubkey) { |
|
|
|
|
try { |
|
|
|
|
const userRelayList = await client.fetchRelayList(userPubkey) |
|
|
|
|
const userInboxes = (userRelayList.read || []).slice(0, 10) // Limit to 10
|
|
|
|
|
userInboxes.forEach(url => { |
|
|
|
|
const normalized = normalizeUrl(url) |
|
|
|
|
if (normalized) relayUrls.add(normalized) |
|
|
|
|
}) |
|
|
|
|
logger.debug('[EventService] Added user inboxes', { |
|
|
|
|
count: userInboxes.length |
|
|
|
|
}) |
|
|
|
|
} catch (error) { |
|
|
|
|
logger.debug('[EventService] Failed to fetch user relay list', { error }) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 5. Add default fast read relays as fallback
|
|
|
|
|
FAST_READ_RELAY_URLS.forEach(url => { |
|
|
|
|
const normalized = normalizeUrl(url) |
|
|
|
|
if (normalized) relayUrls.add(normalized) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
return Array.from(relayUrls) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export class EventService { |
|
|
|
|
private queryService: QueryService |
|
|
|
|
@ -129,6 +198,39 @@ export class EventService {
@@ -129,6 +198,39 @@ export class EventService {
|
|
|
|
|
return results |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Extract relay hints from event tags |
|
|
|
|
* Relay hints are in the 3rd position (index 2) of e, a, q, etc. tags |
|
|
|
|
* Also checks for a dedicated "relays" tag |
|
|
|
|
*/ |
|
|
|
|
private extractRelayHintsFromEvent(event: NEvent | undefined): string[] { |
|
|
|
|
if (!event) return [] |
|
|
|
|
const hints = new Set<string>() |
|
|
|
|
|
|
|
|
|
// Extract from e, a, q tags (relay hint is in position 2, index 2)
|
|
|
|
|
const tagTypesWithRelayHints = ['e', 'a', 'q'] |
|
|
|
|
for (const tag of event.tags) { |
|
|
|
|
if (tagTypesWithRelayHints.includes(tag[0]) && tag.length > 2 && typeof tag[2] === 'string') { |
|
|
|
|
const hint = tag[2] |
|
|
|
|
if (hint.startsWith('wss://') || hint.startsWith('ws://')) { |
|
|
|
|
hints.add(hint) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Also check for dedicated "relays" tag
|
|
|
|
|
const relaysTag = event.tags.find(tag => tag[0] === 'relays') |
|
|
|
|
if (relaysTag && relaysTag.length > 1) { |
|
|
|
|
relaysTag.slice(1).forEach(url => { |
|
|
|
|
if (typeof url === 'string' && (url.startsWith('wss://') || url.startsWith('ws://'))) { |
|
|
|
|
hints.add(url) |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return Array.from(hints) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Clear all in-memory event caches |
|
|
|
|
*/ |
|
|
|
|
@ -180,32 +282,34 @@ export class EventService {
@@ -180,32 +282,34 @@ export class EventService {
|
|
|
|
|
const cached = await indexedDb.getEventFromPublicationStore(filter.ids[0]) |
|
|
|
|
if (cached) { |
|
|
|
|
this.addEventToCache(cached) |
|
|
|
|
// Extract relay hints from cached event's tags (e, a, q tags)
|
|
|
|
|
const eventRelayHints = this.extractRelayHintsFromEvent(cached) |
|
|
|
|
if (eventRelayHints.length > 0) { |
|
|
|
|
relays = [...new Set([...relays, ...eventRelayHints])] |
|
|
|
|
} |
|
|
|
|
return cached |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Try big relays first
|
|
|
|
|
// Try big relays first (uses user's inboxes + defaults)
|
|
|
|
|
if (filter.ids?.length) { |
|
|
|
|
const event = await this.fetchEventFromBigRelaysDataloader.load(filter.ids[0]) |
|
|
|
|
if (event) { |
|
|
|
|
this.addEventToCache(event) |
|
|
|
|
// Extract relay hints from found event's tags (e, a, q tags)
|
|
|
|
|
const eventRelayHints = this.extractRelayHintsFromEvent(event) |
|
|
|
|
if (eventRelayHints.length > 0) { |
|
|
|
|
relays = [...new Set([...relays, ...eventRelayHints])] |
|
|
|
|
} |
|
|
|
|
return event |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Try harder with specified relays or author relays
|
|
|
|
|
if (filter.ids?.length && relays.length) { |
|
|
|
|
const event = await this.tryHarderToFetchEvent(relays, filter, true) |
|
|
|
|
if (event) { |
|
|
|
|
this.addEventToCache(event) |
|
|
|
|
return event |
|
|
|
|
} |
|
|
|
|
} else if (filter.authors?.length) { |
|
|
|
|
const event = await this.tryHarderToFetchEvent(relays, filter, false) |
|
|
|
|
if (event) { |
|
|
|
|
this.addEventToCache(event) |
|
|
|
|
return event |
|
|
|
|
} |
|
|
|
|
// Always try comprehensive relay list (author's outboxes + user's inboxes + hints + seen + defaults)
|
|
|
|
|
const event = await this.tryHarderToFetchEvent(relays, filter, true) |
|
|
|
|
if (event) { |
|
|
|
|
this.addEventToCache(event) |
|
|
|
|
return event |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return undefined |
|
|
|
|
@ -213,19 +317,36 @@ export class EventService {
@@ -213,19 +317,36 @@ export class EventService {
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Private: Try harder to fetch event from relays |
|
|
|
|
* ALWAYS uses: author's outboxes + user's inboxes + relay hints + seen relays + defaults |
|
|
|
|
*/ |
|
|
|
|
private async tryHarderToFetchEvent( |
|
|
|
|
relayUrls: string[], |
|
|
|
|
relayHints: string[], |
|
|
|
|
filter: Filter, |
|
|
|
|
alreadyFetchedFromBigRelays = false |
|
|
|
|
): Promise<NEvent | undefined> { |
|
|
|
|
if (!relayUrls.length && filter.authors?.length) { |
|
|
|
|
// Would need relay list service - for now use big relays
|
|
|
|
|
relayUrls = FAST_READ_RELAY_URLS |
|
|
|
|
} else if (!relayUrls.length && !alreadyFetchedFromBigRelays) { |
|
|
|
|
relayUrls = FAST_READ_RELAY_URLS |
|
|
|
|
// Get seen relays if we have an event ID
|
|
|
|
|
const seenRelays = filter.ids?.length ? client.getSeenEventRelayUrls(filter.ids[0]) : [] |
|
|
|
|
|
|
|
|
|
// Get author pubkey
|
|
|
|
|
const authorPubkey = filter.authors?.length === 1 ? filter.authors[0] : undefined |
|
|
|
|
|
|
|
|
|
// Build comprehensive relay list
|
|
|
|
|
const relayUrls = await buildComprehensiveRelayList(authorPubkey, relayHints, seenRelays) |
|
|
|
|
|
|
|
|
|
if (!relayUrls.length) { |
|
|
|
|
// Fallback to default relays if comprehensive list is empty
|
|
|
|
|
if (!alreadyFetchedFromBigRelays) { |
|
|
|
|
return undefined |
|
|
|
|
} |
|
|
|
|
return undefined |
|
|
|
|
} |
|
|
|
|
if (!relayUrls.length) return undefined |
|
|
|
|
|
|
|
|
|
logger.debug('[EventService] Using comprehensive relay list', { |
|
|
|
|
author: authorPubkey?.substring(0, 8), |
|
|
|
|
relayCount: relayUrls.length, |
|
|
|
|
hasHints: relayHints.length > 0, |
|
|
|
|
hasSeen: seenRelays.length > 0 |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
const isSingleEventById = filter.ids && filter.ids.length === 1 && filter.limit === 1 |
|
|
|
|
const events = await this.queryService.query(relayUrls, filter, undefined, { |
|
|
|
|
@ -238,10 +359,12 @@ export class EventService {
@@ -238,10 +359,12 @@ export class EventService {
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Private: Fetch events from big relays (batch) |
|
|
|
|
* Uses comprehensive relay list: user's inboxes + defaults |
|
|
|
|
*/ |
|
|
|
|
private async fetchEventsFromBigRelays(ids: readonly string[]): Promise<(NEvent | undefined)[]> { |
|
|
|
|
const initialRelays = FAST_READ_RELAY_URLS |
|
|
|
|
const relayUrls = initialRelays.length > 0 ? initialRelays : FAST_READ_RELAY_URLS |
|
|
|
|
// Build comprehensive relay list (user's inboxes + defaults)
|
|
|
|
|
// Note: For batch fetches, we don't have author info, so we use user's inboxes + defaults
|
|
|
|
|
const relayUrls = await buildComprehensiveRelayList(undefined, [], []) |
|
|
|
|
|
|
|
|
|
const isSingleEventFetch = ids.length === 1 |
|
|
|
|
const events = await this.queryService.query(relayUrls, { |
|
|
|
|
@ -256,6 +379,8 @@ export class EventService {
@@ -256,6 +379,8 @@ export class EventService {
|
|
|
|
|
const eventsMap = new Map<string, NEvent>() |
|
|
|
|
for (const event of events) { |
|
|
|
|
eventsMap.set(event.id, event) |
|
|
|
|
// Note: We can't track which relay returned which event in batch queries,
|
|
|
|
|
// but events are still cached and will be found in future queries
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return ids.map((id) => eventsMap.get(id)) |
|
|
|
|
|