|
|
|
@ -653,7 +653,12 @@ class ClientService extends EventTarget { |
|
|
|
.slice(0, MAX_PUBLISH_RELAYS) |
|
|
|
.slice(0, MAX_PUBLISH_RELAYS) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private async capPublishRelayUrlsForPublish( |
|
|
|
/** |
|
|
|
|
|
|
|
* Same ordering as {@link prioritizePublishUrlList} but bounded so relay-list / inbox fetches |
|
|
|
|
|
|
|
* cannot block publishing indefinitely. Used from {@link determineTargetRelays} (before |
|
|
|
|
|
|
|
* {@link publishEvent}) and from {@link capPublishRelayUrlsForPublish}. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
private async prioritizePublishUrlListWithTimeout( |
|
|
|
relayUrls: string[], |
|
|
|
relayUrls: string[], |
|
|
|
event: NEvent, |
|
|
|
event: NEvent, |
|
|
|
favoriteRelayUrls: string[] = [] |
|
|
|
favoriteRelayUrls: string[] = [] |
|
|
|
@ -675,6 +680,72 @@ class ClientService extends EventTarget { |
|
|
|
]) |
|
|
|
]) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private async capPublishRelayUrlsForPublish( |
|
|
|
|
|
|
|
relayUrls: string[], |
|
|
|
|
|
|
|
event: NEvent, |
|
|
|
|
|
|
|
favoriteRelayUrls: string[] = [] |
|
|
|
|
|
|
|
): Promise<string[]> { |
|
|
|
|
|
|
|
return this.prioritizePublishUrlListWithTimeout(relayUrls, event, favoriteRelayUrls) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private emptyRelayListForPublish(): TRelayList { |
|
|
|
|
|
|
|
return { |
|
|
|
|
|
|
|
write: [], |
|
|
|
|
|
|
|
read: [], |
|
|
|
|
|
|
|
originalRelays: [], |
|
|
|
|
|
|
|
httpRead: [], |
|
|
|
|
|
|
|
httpWrite: [], |
|
|
|
|
|
|
|
httpOriginalRelays: [] |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Bounded wait so NIP-65 fetch cannot block publishing (reactions, replies without relay picker, etc.). */ |
|
|
|
|
|
|
|
private async fetchRelayListWithPublishTimeout(pubkey: string): Promise<TRelayList> { |
|
|
|
|
|
|
|
const empty = this.emptyRelayListForPublish() |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
return await Promise.race([ |
|
|
|
|
|
|
|
this.fetchRelayList(pubkey), |
|
|
|
|
|
|
|
new Promise<TRelayList>((resolve) => |
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
|
|
|
|
|
logger.warn('[DetermineTargetRelays] fetchRelayList timed out; using empty outbox', { |
|
|
|
|
|
|
|
pubkeySlice: pubkey.slice(0, 12) |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
resolve(empty) |
|
|
|
|
|
|
|
}, PUBLISH_RELAY_LIST_RESOLUTION_TIMEOUT_MS) |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
]) |
|
|
|
|
|
|
|
} catch (err) { |
|
|
|
|
|
|
|
logger.warn('[DetermineTargetRelays] fetchRelayList failed, using fallback relays', { |
|
|
|
|
|
|
|
pubkeySlice: pubkey.slice(0, 12), |
|
|
|
|
|
|
|
error: err instanceof Error ? err.message : String(err) |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
return empty |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private async fetchRelayListsWithPublishTimeout(pubkeys: string[]): Promise<TRelayList[]> { |
|
|
|
|
|
|
|
if (pubkeys.length === 0) return [] |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
return await Promise.race([ |
|
|
|
|
|
|
|
this.fetchRelayLists(pubkeys), |
|
|
|
|
|
|
|
new Promise<TRelayList[]>((resolve) => |
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
|
|
|
|
|
logger.warn('[DetermineTargetRelays] fetchRelayLists timed out; skipping context inbox merge', { |
|
|
|
|
|
|
|
pubkeyCount: pubkeys.length |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
resolve(pubkeys.map(() => this.emptyRelayListForPublish())) |
|
|
|
|
|
|
|
}, PUBLISH_RELAY_LIST_RESOLUTION_TIMEOUT_MS) |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
]) |
|
|
|
|
|
|
|
} catch (err) { |
|
|
|
|
|
|
|
logger.warn('[DetermineTargetRelays] fetchRelayLists failed', { |
|
|
|
|
|
|
|
pubkeyCount: pubkeys.length, |
|
|
|
|
|
|
|
error: err instanceof Error ? err.message : String(err) |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
return pubkeys.map(() => this.emptyRelayListForPublish()) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* Determine which relays to publish an event to. |
|
|
|
* Determine which relays to publish an event to. |
|
|
|
* Fallbacks (used when user relay list is empty or fetch fails): |
|
|
|
* Fallbacks (used when user relay list is empty or fetch fails): |
|
|
|
@ -703,7 +774,7 @@ class ClientService extends EventTarget { |
|
|
|
// For Report events, always include user's write relays first, then add seen relays if they're write-capable
|
|
|
|
// For Report events, always include user's write relays first, then add seen relays if they're write-capable
|
|
|
|
if (event.kind === kinds.Report) { |
|
|
|
if (event.kind === kinds.Report) { |
|
|
|
// Start with user's write relays (outboxes) - these are the primary targets for reports
|
|
|
|
// Start with user's write relays (outboxes) - these are the primary targets for reports
|
|
|
|
const relayList = await this.fetchRelayList(event.pubkey) |
|
|
|
const relayList = await this.fetchRelayListWithPublishTimeout(event.pubkey) |
|
|
|
const reportHttpWrites = (relayList?.httpWrite ?? []) |
|
|
|
const reportHttpWrites = (relayList?.httpWrite ?? []) |
|
|
|
.map((url) => normalizeHttpRelayUrl(url) || url) |
|
|
|
.map((url) => normalizeHttpRelayUrl(url) || url) |
|
|
|
.filter((u): u is string => !!u) |
|
|
|
.filter((u): u is string => !!u) |
|
|
|
@ -757,7 +828,19 @@ class ClientService extends EventTarget { |
|
|
|
event.kind === ExtendedKind.PUBLIC_MESSAGE || |
|
|
|
event.kind === ExtendedKind.PUBLIC_MESSAGE || |
|
|
|
event.kind === ExtendedKind.CALENDAR_EVENT_RSVP |
|
|
|
event.kind === ExtendedKind.CALENDAR_EVENT_RSVP |
|
|
|
) { |
|
|
|
) { |
|
|
|
const authorRelayList = await this.fetchRelayList(event.pubkey).catch(() => ({ write: [] as string[], read: [] as string[], httpWrite: [] as string[], httpRead: [] as string[] })) |
|
|
|
const recipientPubkeys = Array.from( |
|
|
|
|
|
|
|
new Set( |
|
|
|
|
|
|
|
event.tags.filter((t) => t[0] === 'p' && t[1] && isValidPubkey(t[1])).map((t) => t[1] as string) |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
).filter((p) => p !== event.pubkey) |
|
|
|
|
|
|
|
const recipientListsPromise = |
|
|
|
|
|
|
|
recipientPubkeys.length > 0 |
|
|
|
|
|
|
|
? this.fetchRelayListsWithPublishTimeout(recipientPubkeys) |
|
|
|
|
|
|
|
: Promise.resolve([] as TRelayList[]) |
|
|
|
|
|
|
|
const [authorRelayList, recipientRelayLists] = await Promise.all([ |
|
|
|
|
|
|
|
this.fetchRelayListWithPublishTimeout(event.pubkey), |
|
|
|
|
|
|
|
recipientListsPromise |
|
|
|
|
|
|
|
]) |
|
|
|
const authorHttpWrites = (authorRelayList?.httpWrite ?? []) |
|
|
|
const authorHttpWrites = (authorRelayList?.httpWrite ?? []) |
|
|
|
.map((url) => normalizeHttpRelayUrl(url)) |
|
|
|
.map((url) => normalizeHttpRelayUrl(url)) |
|
|
|
.filter((url): url is string => !!url) |
|
|
|
.filter((url): url is string => !!url) |
|
|
|
@ -768,20 +851,12 @@ class ClientService extends EventTarget { |
|
|
|
if (authorWrite.length === 0) { |
|
|
|
if (authorWrite.length === 0) { |
|
|
|
authorWrite = [...FAST_WRITE_RELAY_URLS] |
|
|
|
authorWrite = [...FAST_WRITE_RELAY_URLS] |
|
|
|
} |
|
|
|
} |
|
|
|
const recipientPubkeys = Array.from( |
|
|
|
|
|
|
|
new Set( |
|
|
|
|
|
|
|
event.tags.filter((t) => t[0] === 'p' && t[1] && isValidPubkey(t[1])).map((t) => t[1] as string) |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
).filter((p) => p !== event.pubkey) |
|
|
|
|
|
|
|
let recipientRead: string[] = [] |
|
|
|
let recipientRead: string[] = [] |
|
|
|
if (recipientPubkeys.length > 0) { |
|
|
|
|
|
|
|
const recipientRelayLists = await this.fetchRelayLists(recipientPubkeys) |
|
|
|
|
|
|
|
recipientRead = recipientRelayLists.flatMap((rl) => [ |
|
|
|
recipientRead = recipientRelayLists.flatMap((rl) => [ |
|
|
|
...(rl?.httpRead ?? []).map((url) => normalizeHttpRelayUrl(url)).filter((u): u is string => !!u && !isLocalNetworkUrl(u)), |
|
|
|
...(rl?.httpRead ?? []).map((url) => normalizeHttpRelayUrl(url)).filter((u): u is string => !!u && !isLocalNetworkUrl(u)), |
|
|
|
...(rl?.read ?? []).map((url) => normalizeUrl(url)).filter((u): u is string => !!u && !isLocalNetworkUrl(u)) |
|
|
|
...(rl?.read ?? []).map((url) => normalizeUrl(url)).filter((u): u is string => !!u && !isLocalNetworkUrl(u)) |
|
|
|
]) |
|
|
|
]) |
|
|
|
recipientRead = dedupeNormalizeRelayUrlsOrdered(recipientRead) |
|
|
|
recipientRead = dedupeNormalizeRelayUrlsOrdered(recipientRead) |
|
|
|
} |
|
|
|
|
|
|
|
let pubRelays = mergeRelayPriorityLayers( |
|
|
|
let pubRelays = mergeRelayPriorityLayers( |
|
|
|
[relayUrlsLocalsFirst(authorWrite), dedupeNormalizeRelayUrlsOrdered(recipientRead)], |
|
|
|
[relayUrlsLocalsFirst(authorWrite), dedupeNormalizeRelayUrlsOrdered(recipientRead)], |
|
|
|
blockedRelayUrls, |
|
|
|
blockedRelayUrls, |
|
|
|
@ -818,20 +893,13 @@ class ClientService extends EventTarget { |
|
|
|
if (event.kind === ExtendedKind.SPELL) { |
|
|
|
if (event.kind === ExtendedKind.SPELL) { |
|
|
|
let spellRelayList: TRelayList | undefined |
|
|
|
let spellRelayList: TRelayList | undefined |
|
|
|
try { |
|
|
|
try { |
|
|
|
spellRelayList = await this.fetchRelayList(event.pubkey) |
|
|
|
spellRelayList = await this.fetchRelayListWithPublishTimeout(event.pubkey) |
|
|
|
} catch (err) { |
|
|
|
} catch (err) { |
|
|
|
logger.warn('[DetermineTargetRelays] fetchRelayList failed for spell', { |
|
|
|
logger.warn('[DetermineTargetRelays] fetchRelayList failed for spell', { |
|
|
|
pubkey: event.pubkey, |
|
|
|
pubkey: event.pubkey, |
|
|
|
error: err instanceof Error ? err.message : String(err) |
|
|
|
error: err instanceof Error ? err.message : String(err) |
|
|
|
}) |
|
|
|
}) |
|
|
|
spellRelayList = { |
|
|
|
spellRelayList = this.emptyRelayListForPublish() |
|
|
|
write: [], |
|
|
|
|
|
|
|
read: [], |
|
|
|
|
|
|
|
originalRelays: [], |
|
|
|
|
|
|
|
httpRead: [], |
|
|
|
|
|
|
|
httpWrite: [], |
|
|
|
|
|
|
|
httpOriginalRelays: [] |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
const spellHttpWrites = (spellRelayList?.httpWrite ?? []) |
|
|
|
const spellHttpWrites = (spellRelayList?.httpWrite ?? []) |
|
|
|
.map((url) => normalizeHttpRelayUrl(url)) |
|
|
|
.map((url) => normalizeHttpRelayUrl(url)) |
|
|
|
@ -862,25 +930,26 @@ class ClientService extends EventTarget { |
|
|
|
|
|
|
|
|
|
|
|
const bootstrapExtras: string[] = [...(additionalRelayUrls ?? [])] |
|
|
|
const bootstrapExtras: string[] = [...(additionalRelayUrls ?? [])] |
|
|
|
let authorInboxFromContext: string[] = [] |
|
|
|
let authorInboxFromContext: string[] = [] |
|
|
|
if ( |
|
|
|
const shouldMergeContextInboxes = |
|
|
|
!specifiedRelayUrls?.length && |
|
|
|
!specifiedRelayUrls?.length && |
|
|
|
![kinds.Contacts, kinds.Mutelist, ExtendedKind.FOLLOW_SET].includes(event.kind) |
|
|
|
![kinds.Contacts, kinds.Mutelist, ExtendedKind.FOLLOW_SET].includes(event.kind) |
|
|
|
) { |
|
|
|
const ctxPubkeys = shouldMergeContextInboxes ? this.collectReplyAndMentionPubkeys(event) : [] |
|
|
|
const ctxPubkeys = this.collectReplyAndMentionPubkeys(event) |
|
|
|
const relayListsPromise = |
|
|
|
if (ctxPubkeys.length > 0) { |
|
|
|
ctxPubkeys.length > 0 |
|
|
|
const relayLists = await this.fetchRelayLists(ctxPubkeys) |
|
|
|
? this.fetchRelayListsWithPublishTimeout(ctxPubkeys) |
|
|
|
relayLists.forEach((relayList) => { |
|
|
|
: Promise.resolve([] as TRelayList[]) |
|
|
|
for (const u of relayList.httpRead ?? []) { |
|
|
|
const relayListPromise = this.fetchRelayListWithPublishTimeout(event.pubkey) |
|
|
|
|
|
|
|
const [relayLists, relayList] = await Promise.all([relayListsPromise, relayListPromise]) |
|
|
|
|
|
|
|
relayLists.forEach((rl) => { |
|
|
|
|
|
|
|
for (const u of rl.httpRead ?? []) { |
|
|
|
const n = normalizeHttpRelayUrl(u) || u |
|
|
|
const n = normalizeHttpRelayUrl(u) || u |
|
|
|
if (n) authorInboxFromContext.push(n) |
|
|
|
if (n) authorInboxFromContext.push(n) |
|
|
|
} |
|
|
|
} |
|
|
|
for (const u of relayList.read ?? []) { |
|
|
|
for (const u of rl.read ?? []) { |
|
|
|
const n = normalizeUrl(u) || u |
|
|
|
const n = normalizeUrl(u) || u |
|
|
|
if (n) authorInboxFromContext.push(n) |
|
|
|
if (n) authorInboxFromContext.push(n) |
|
|
|
} |
|
|
|
} |
|
|
|
}) |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if ( |
|
|
|
if ( |
|
|
|
[ |
|
|
|
[ |
|
|
|
kinds.RelayList, |
|
|
|
kinds.RelayList, |
|
|
|
@ -918,34 +987,9 @@ class ClientService extends EventTarget { |
|
|
|
event.kind === ExtendedKind.FAVORITE_RELAYS || |
|
|
|
event.kind === ExtendedKind.FAVORITE_RELAYS || |
|
|
|
event.kind === kinds.Relaysets |
|
|
|
event.kind === kinds.Relaysets |
|
|
|
) { |
|
|
|
) { |
|
|
|
logger.debug('[DetermineTargetRelays] Fetching user relay list for event publication', { |
|
|
|
logger.debug('[DetermineTargetRelays] User relay list resolved for publication', { |
|
|
|
pubkey: event.pubkey, |
|
|
|
pubkey: event.pubkey, |
|
|
|
kind: event.kind |
|
|
|
kind: event.kind, |
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
let relayList: TRelayList | undefined |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
relayList = await this.fetchRelayList(event.pubkey) |
|
|
|
|
|
|
|
} catch (err) { |
|
|
|
|
|
|
|
logger.warn('[DetermineTargetRelays] fetchRelayList failed, using fallback relays', { |
|
|
|
|
|
|
|
pubkey: event.pubkey, |
|
|
|
|
|
|
|
error: err instanceof Error ? err.message : String(err) |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
relayList = { |
|
|
|
|
|
|
|
write: [], |
|
|
|
|
|
|
|
read: [], |
|
|
|
|
|
|
|
originalRelays: [], |
|
|
|
|
|
|
|
httpRead: [], |
|
|
|
|
|
|
|
httpWrite: [], |
|
|
|
|
|
|
|
httpOriginalRelays: [] |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if ( |
|
|
|
|
|
|
|
event.kind === kinds.RelayList || |
|
|
|
|
|
|
|
event.kind === ExtendedKind.FAVORITE_RELAYS || |
|
|
|
|
|
|
|
event.kind === kinds.Relaysets |
|
|
|
|
|
|
|
) { |
|
|
|
|
|
|
|
logger.debug('[DetermineTargetRelays] User relay list fetched', { |
|
|
|
|
|
|
|
hasRelayList: !!relayList, |
|
|
|
hasRelayList: !!relayList, |
|
|
|
writeRelayCount: relayList?.write?.length ?? 0, |
|
|
|
writeRelayCount: relayList?.write?.length ?? 0, |
|
|
|
readRelayCount: relayList?.read?.length ?? 0, |
|
|
|
readRelayCount: relayList?.read?.length ?? 0, |
|
|
|
@ -1000,7 +1044,7 @@ class ClientService extends EventTarget { |
|
|
|
|
|
|
|
|
|
|
|
if (specifiedRelayUrls?.length) { |
|
|
|
if (specifiedRelayUrls?.length) { |
|
|
|
const checkedCount = specifiedRelayUrls.length |
|
|
|
const checkedCount = specifiedRelayUrls.length |
|
|
|
relays = await this.prioritizePublishUrlList(relays, event, favoriteRelayUrls ?? []) |
|
|
|
relays = await this.prioritizePublishUrlListWithTimeout(relays, event, favoriteRelayUrls ?? []) |
|
|
|
if (checkedCount > relays.length) { |
|
|
|
if (checkedCount > relays.length) { |
|
|
|
logger.info('[Publish] Relay picker: checked count exceeds per-publish cap (stage 1)', { |
|
|
|
logger.info('[Publish] Relay picker: checked count exceeds per-publish cap (stage 1)', { |
|
|
|
checkedInRelayPicker: checkedCount, |
|
|
|
checkedInRelayPicker: checkedCount, |
|
|
|
@ -3330,39 +3374,28 @@ class ClientService extends EventTarget { |
|
|
|
return requestPromise |
|
|
|
return requestPromise |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async fetchRelayLists(pubkeys: string[]): Promise<TRelayList[]> { |
|
|
|
/** |
|
|
|
// First check IndexedDB for offline/quick access (prioritizes cache relays for offline use)
|
|
|
|
* Merge NIP-65 (10002), HTTP relay list (10243), and cache relays (10432) from network and/or IndexedDB. |
|
|
|
const storedRelayEvents = await Promise.all( |
|
|
|
* Network arrays may be sparse/undefined per index; stored* always filled from IDB reads. |
|
|
|
pubkeys.map(pubkey => indexedDb.getReplaceableEvent(pubkey, kinds.RelayList)) |
|
|
|
*/ |
|
|
|
) |
|
|
|
private mergeRelayListsBundle( |
|
|
|
const storedCacheRelayEvents = await Promise.all( |
|
|
|
pubkeys: string[], |
|
|
|
pubkeys.map(pubkey => indexedDb.getReplaceableEvent(pubkey, ExtendedKind.CACHE_RELAYS)) |
|
|
|
relayEvents: (NEvent | null | undefined)[], |
|
|
|
) |
|
|
|
httpRelayEvents: (NEvent | null | undefined)[], |
|
|
|
const storedHttpRelayEvents = await Promise.all( |
|
|
|
cacheRelayEvents: (NEvent | null | undefined)[], |
|
|
|
pubkeys.map(pubkey => indexedDb.getReplaceableEvent(pubkey, ExtendedKind.HTTP_RELAY_LIST)) |
|
|
|
storedRelayEvents: (NEvent | null | undefined)[], |
|
|
|
) |
|
|
|
storedHttpRelayEvents: (NEvent | null | undefined)[], |
|
|
|
|
|
|
|
storedCacheRelayEvents: (NEvent | null | undefined)[] |
|
|
|
// Then fetch from relays (will update cache if newer)
|
|
|
|
): TRelayList[] { |
|
|
|
const relayEvents = await this.replaceableEventService.fetchReplaceableEventsFromProfileFetchRelays(pubkeys, kinds.RelayList) |
|
|
|
|
|
|
|
const httpRelayEvents = await this.replaceableEventService.fetchReplaceableEventsFromProfileFetchRelays( |
|
|
|
|
|
|
|
pubkeys, |
|
|
|
|
|
|
|
ExtendedKind.HTTP_RELAY_LIST |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Fetch cache relays from multiple sources: FAST_READ_RELAY_URLS, PROFILE_RELAY_URLS, and user's inboxes/outboxes
|
|
|
|
|
|
|
|
const cacheRelayEvents = await this.fetchCacheRelayEventsFromMultipleSources(pubkeys, relayEvents, storedRelayEvents) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return pubkeys.map((targetPubkey, index) => { |
|
|
|
return pubkeys.map((targetPubkey, index) => { |
|
|
|
const isOwnRelayList = |
|
|
|
const isOwnRelayList = |
|
|
|
this.pubkey != null && hexPubkeysEqual(this.pubkey, userIdToPubkey(targetPubkey)) |
|
|
|
this.pubkey != null && hexPubkeysEqual(this.pubkey, userIdToPubkey(targetPubkey)) |
|
|
|
|
|
|
|
|
|
|
|
// Use stored cache relay event if available (for offline), otherwise use fetched one
|
|
|
|
|
|
|
|
const storedCacheEvent = storedCacheRelayEvents[index] |
|
|
|
const storedCacheEvent = storedCacheRelayEvents[index] |
|
|
|
const cacheEvent = cacheRelayEvents[index] || storedCacheEvent |
|
|
|
const cacheEvent = cacheRelayEvents[index] || storedCacheEvent |
|
|
|
|
|
|
|
|
|
|
|
const httpRelayEvent = httpRelayEvents[index] || storedHttpRelayEvents[index] |
|
|
|
const httpRelayEvent = httpRelayEvents[index] || storedHttpRelayEvents[index] |
|
|
|
|
|
|
|
|
|
|
|
// Use stored relay event if no network event (for offline), otherwise use fetched one
|
|
|
|
|
|
|
|
const storedRelayEvent = storedRelayEvents[index] |
|
|
|
const storedRelayEvent = storedRelayEvents[index] |
|
|
|
const relayEvent = relayEvents[index] || storedRelayEvent |
|
|
|
const relayEvent = relayEvents[index] || storedRelayEvent |
|
|
|
|
|
|
|
|
|
|
|
@ -3388,27 +3421,22 @@ class ClientService extends EventTarget { |
|
|
|
...emptyHttp |
|
|
|
...emptyHttp |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Merge kind 10432 (cache relays) only for the logged-in user — never use someone else's local relays.
|
|
|
|
|
|
|
|
if (isOwnRelayList && cacheEvent) { |
|
|
|
if (isOwnRelayList && cacheEvent) { |
|
|
|
const cacheRelayList = getRelayListFromEvent(cacheEvent) |
|
|
|
const cacheRelayList = getRelayListFromEvent(cacheEvent) |
|
|
|
|
|
|
|
|
|
|
|
// Merge read relays - cache relays first, then others (for offline priority)
|
|
|
|
|
|
|
|
const mergedRead = [...cacheRelayList.read, ...relayList.read] |
|
|
|
const mergedRead = [...cacheRelayList.read, ...relayList.read] |
|
|
|
const mergedWrite = [...cacheRelayList.write, ...relayList.write] |
|
|
|
const mergedWrite = [...cacheRelayList.write, ...relayList.write] |
|
|
|
const mergedOriginalRelays = new Map<string, TMailboxRelay>() |
|
|
|
const mergedOriginalRelays = new Map<string, TMailboxRelay>() |
|
|
|
|
|
|
|
|
|
|
|
// Add cache relay original relays first (prioritized)
|
|
|
|
cacheRelayList.originalRelays.forEach((relay) => { |
|
|
|
cacheRelayList.originalRelays.forEach(relay => { |
|
|
|
|
|
|
|
mergedOriginalRelays.set(relay.url, relay) |
|
|
|
mergedOriginalRelays.set(relay.url, relay) |
|
|
|
}) |
|
|
|
}) |
|
|
|
// Then add regular relay original relays
|
|
|
|
relayList.originalRelays.forEach((relay) => { |
|
|
|
relayList.originalRelays.forEach(relay => { |
|
|
|
|
|
|
|
if (!mergedOriginalRelays.has(relay.url)) { |
|
|
|
if (!mergedOriginalRelays.has(relay.url)) { |
|
|
|
mergedOriginalRelays.set(relay.url, relay) |
|
|
|
mergedOriginalRelays.set(relay.url, relay) |
|
|
|
} |
|
|
|
} |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
// Deduplicate while preserving order (cache relays first)
|
|
|
|
|
|
|
|
return mergeKind10243({ |
|
|
|
return mergeKind10243({ |
|
|
|
write: Array.from(new Set(mergedWrite)), |
|
|
|
write: Array.from(new Set(mergedWrite)), |
|
|
|
read: Array.from(new Set(mergedRead)), |
|
|
|
read: Array.from(new Set(mergedRead)), |
|
|
|
@ -3417,7 +3445,6 @@ class ClientService extends EventTarget { |
|
|
|
}) |
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// If no merged cache path, return original relay list or default (with own cache as fallback only)
|
|
|
|
|
|
|
|
if (!relayEvent) { |
|
|
|
if (!relayEvent) { |
|
|
|
if (isOwnRelayList && storedCacheEvent) { |
|
|
|
if (isOwnRelayList && storedCacheEvent) { |
|
|
|
const cacheRelayList = getRelayListFromEvent(storedCacheEvent) |
|
|
|
const cacheRelayList = getRelayListFromEvent(storedCacheEvent) |
|
|
|
@ -3444,6 +3471,119 @@ class ClientService extends EventTarget { |
|
|
|
}) |
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Background refresh so UI/publish can use IDB immediately while relays catch up. */ |
|
|
|
|
|
|
|
private refreshRelayListsFromNetwork( |
|
|
|
|
|
|
|
pubkeys: string[], |
|
|
|
|
|
|
|
storedKind10002: (NEvent | null | undefined)[] |
|
|
|
|
|
|
|
): void { |
|
|
|
|
|
|
|
void (async () => { |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
const relayEvents = await this.replaceableEventService.fetchReplaceableEventsFromProfileFetchRelays( |
|
|
|
|
|
|
|
pubkeys, |
|
|
|
|
|
|
|
kinds.RelayList |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
await this.replaceableEventService.fetchReplaceableEventsFromProfileFetchRelays( |
|
|
|
|
|
|
|
pubkeys, |
|
|
|
|
|
|
|
ExtendedKind.HTTP_RELAY_LIST |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
await this.fetchCacheRelayEventsFromMultipleSources(pubkeys, relayEvents, storedKind10002) |
|
|
|
|
|
|
|
} catch { |
|
|
|
|
|
|
|
/* best-effort */ |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
})() |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async fetchRelayLists(pubkeys: string[]): Promise<TRelayList[]> { |
|
|
|
|
|
|
|
if (pubkeys.length === 0) return [] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const storedRelayEvents = await Promise.all( |
|
|
|
|
|
|
|
pubkeys.map((pubkey) => indexedDb.getReplaceableEvent(pubkey, kinds.RelayList)) |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
const storedCacheRelayEvents = await Promise.all( |
|
|
|
|
|
|
|
pubkeys.map((pubkey) => indexedDb.getReplaceableEvent(pubkey, ExtendedKind.CACHE_RELAYS)) |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
const storedHttpRelayEvents = await Promise.all( |
|
|
|
|
|
|
|
pubkeys.map((pubkey) => indexedDb.getReplaceableEvent(pubkey, ExtendedKind.HTTP_RELAY_LIST)) |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const budgetMs = PUBLISH_RELAY_LIST_RESOLUTION_TIMEOUT_MS |
|
|
|
|
|
|
|
const allHaveKind10002 = pubkeys.every((_, i) => storedRelayEvents[i] != null) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const networkBundle = async (): Promise<{ |
|
|
|
|
|
|
|
relayEvents: (NEvent | null | undefined)[] |
|
|
|
|
|
|
|
httpRelayEvents: (NEvent | null | undefined)[] |
|
|
|
|
|
|
|
cacheRelayEvents: (NEvent | null | undefined)[] |
|
|
|
|
|
|
|
}> => { |
|
|
|
|
|
|
|
const relayEvents = await this.replaceableEventService.fetchReplaceableEventsFromProfileFetchRelays( |
|
|
|
|
|
|
|
pubkeys, |
|
|
|
|
|
|
|
kinds.RelayList |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
const httpRelayEvents = await this.replaceableEventService.fetchReplaceableEventsFromProfileFetchRelays( |
|
|
|
|
|
|
|
pubkeys, |
|
|
|
|
|
|
|
ExtendedKind.HTTP_RELAY_LIST |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
const cacheRelayEvents = await this.fetchCacheRelayEventsFromMultipleSources( |
|
|
|
|
|
|
|
pubkeys, |
|
|
|
|
|
|
|
relayEvents, |
|
|
|
|
|
|
|
storedRelayEvents |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
return { relayEvents, httpRelayEvents, cacheRelayEvents } |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (allHaveKind10002) { |
|
|
|
|
|
|
|
this.refreshRelayListsFromNetwork(pubkeys, storedRelayEvents) |
|
|
|
|
|
|
|
logger.debug( |
|
|
|
|
|
|
|
'[FetchRelayLists] Kind 10002 present in IndexedDB for all pubkeys; merging locally, network refresh in background', |
|
|
|
|
|
|
|
{ count: pubkeys.length } |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
const cacheRelayEvents = await Promise.race([ |
|
|
|
|
|
|
|
this.fetchCacheRelayEventsFromMultipleSources(pubkeys, storedRelayEvents, storedRelayEvents), |
|
|
|
|
|
|
|
new Promise<(NEvent | null | undefined)[]>((resolve) => |
|
|
|
|
|
|
|
setTimeout(() => resolve(storedCacheRelayEvents.map((e) => e ?? undefined)), budgetMs) |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
]) |
|
|
|
|
|
|
|
return this.mergeRelayListsBundle( |
|
|
|
|
|
|
|
pubkeys, |
|
|
|
|
|
|
|
storedRelayEvents.map((e) => e ?? undefined), |
|
|
|
|
|
|
|
storedHttpRelayEvents.map((e) => e ?? undefined), |
|
|
|
|
|
|
|
cacheRelayEvents, |
|
|
|
|
|
|
|
storedRelayEvents, |
|
|
|
|
|
|
|
storedHttpRelayEvents, |
|
|
|
|
|
|
|
storedCacheRelayEvents |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const raced = await Promise.race([ |
|
|
|
|
|
|
|
networkBundle(), |
|
|
|
|
|
|
|
new Promise<null>((resolve) => setTimeout(() => resolve(null), budgetMs)) |
|
|
|
|
|
|
|
]) |
|
|
|
|
|
|
|
if (raced != null) { |
|
|
|
|
|
|
|
return this.mergeRelayListsBundle( |
|
|
|
|
|
|
|
pubkeys, |
|
|
|
|
|
|
|
raced.relayEvents, |
|
|
|
|
|
|
|
raced.httpRelayEvents, |
|
|
|
|
|
|
|
raced.cacheRelayEvents, |
|
|
|
|
|
|
|
storedRelayEvents, |
|
|
|
|
|
|
|
storedHttpRelayEvents, |
|
|
|
|
|
|
|
storedCacheRelayEvents |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger.warn('[FetchRelayLists] Network relay-list fetch exceeded budget; using IndexedDB / empty network layer only', { |
|
|
|
|
|
|
|
pubkeyCount: pubkeys.length |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
const cacheRelayEvents = storedCacheRelayEvents.map((e) => e ?? undefined) |
|
|
|
|
|
|
|
return this.mergeRelayListsBundle( |
|
|
|
|
|
|
|
pubkeys, |
|
|
|
|
|
|
|
pubkeys.map(() => undefined), |
|
|
|
|
|
|
|
pubkeys.map(() => undefined), |
|
|
|
|
|
|
|
cacheRelayEvents, |
|
|
|
|
|
|
|
storedRelayEvents, |
|
|
|
|
|
|
|
storedHttpRelayEvents, |
|
|
|
|
|
|
|
storedCacheRelayEvents |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async forceUpdateRelayListEvent(pubkey: string) { |
|
|
|
async forceUpdateRelayListEvent(pubkey: string) { |
|
|
|
await this.replaceableEventService.fetchReplaceableEvent(pubkey, kinds.RelayList) |
|
|
|
await this.replaceableEventService.fetchReplaceableEvent(pubkey, kinds.RelayList) |
|
|
|
} |
|
|
|
} |
|
|
|
@ -3471,11 +3611,18 @@ class ClientService extends EventTarget { |
|
|
|
return storedCacheRelayEvents |
|
|
|
return storedCacheRelayEvents |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Fetch from PROFILE_FETCH_RELAY_URLS
|
|
|
|
const cacheRelayEvents = await Promise.race([ |
|
|
|
const cacheRelayEvents = await this.replaceableEventService.fetchReplaceableEventsFromProfileFetchRelays( |
|
|
|
this.replaceableEventService.fetchReplaceableEventsFromProfileFetchRelays( |
|
|
|
pubkeysToFetch, |
|
|
|
pubkeysToFetch, |
|
|
|
ExtendedKind.CACHE_RELAYS |
|
|
|
ExtendedKind.CACHE_RELAYS |
|
|
|
|
|
|
|
), |
|
|
|
|
|
|
|
new Promise<(NEvent | undefined)[]>((resolve) => |
|
|
|
|
|
|
|
setTimeout( |
|
|
|
|
|
|
|
() => resolve(pubkeysToFetch.map(() => undefined)), |
|
|
|
|
|
|
|
PUBLISH_RELAY_LIST_RESOLUTION_TIMEOUT_MS |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
]) |
|
|
|
|
|
|
|
|
|
|
|
// Map results back to original pubkey order
|
|
|
|
// Map results back to original pubkey order
|
|
|
|
return pubkeys.map((pubkey, index) => { |
|
|
|
return pubkeys.map((pubkey, index) => { |
|
|
|
|