Browse Source

more efficiency gains

imwald
Silberengel 1 month ago
parent
commit
18f307c1a9
  1. 14
      src/components/NoteList/index.tsx
  2. 20
      src/components/ReplyNoteList/index.tsx
  3. 1
      src/constants.ts
  4. 17
      src/lib/relay-list-builder.test.ts
  5. 202
      src/lib/relay-list-builder.ts
  6. 115
      src/services/client-replaceable-events.service.ts
  7. 7
      src/services/client.service.ts
  8. 123
      src/services/note-stats.service.ts

14
src/components/NoteList/index.tsx

@ -23,7 +23,7 @@ import { @@ -23,7 +23,7 @@ import {
isSpellSubRequestsSameFiltersDifferentRelays
} from '@/lib/spell-feed-request-identity'
import logger from '@/lib/logger'
import { isLocalNetworkUrl, normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url'
import { isLocalNetworkUrl, normalizeUrl } from '@/lib/url'
import { eventPassesNoteListKindPicker } from '@/lib/feed-kind-filter'
import { collectLocalEventsForTextSearch } from '@/lib/local-nip50-search-merge'
import { eventMatchesNip50LocalFullTextQuery } from '@/lib/nip50-local-text-match'
@ -1520,22 +1520,12 @@ const NoteList = forwardRef( @@ -1520,22 +1520,12 @@ const NoteList = forwardRef(
void (async () => {
if (gen !== feedProfileBatchGenRef.current) return
const contextualReadRelays = Array.from(
new Set(
subRequestsRef.current
.flatMap((r) => r.urls)
.map((u) => normalizeAnyRelayUrl(u) || normalizeUrl(u) || u.trim())
.filter(Boolean)
)
)
const chunks: string[][] = []
for (let i = 0; i < need.length; i += FEED_PROFILE_CHUNK) {
chunks.push(need.slice(i, i + FEED_PROFILE_CHUNK))
}
const settled = await Promise.allSettled(
chunks.map((chunk) =>
client.fetchProfilesForPubkeys(chunk, { contextualReadRelays })
)
chunks.map((chunk) => client.fetchProfilesForPubkeys(chunk))
)
if (gen !== feedProfileBatchGenRef.current) return

20
src/components/ReplyNoteList/index.tsx

@ -658,20 +658,6 @@ function ReplyNoteList({ @@ -658,20 +658,6 @@ function ReplyNoteList({
return zapsThenTimeSorted(merged, 'desc')
}, [replies, filteredQuoteEvents, showQuotes, sort, replyIdSet, rootInfo, event.kind])
/** Relays that actually delivered thread rows — used to resolve kind-0 when profile mirrors do not replicate them. */
const threadProfileContextRelays = useMemo(() => {
const s = new Set<string>()
const addEv = (e: NEvent) => {
for (const u of client.getSeenEventRelayUrls(e.id)) {
const n = normalizeAnyRelayUrl(u) || u
if (n) s.add(n)
}
}
addEv(event)
for (const e of mergedFeed) addEv(e)
return [...s]
}, [event, mergedFeed])
useEffect(() => {
if (!rootInfo) return
const toAdd = filteredQuoteEvents.filter((evt) =>
@ -756,13 +742,12 @@ function ReplyNoteList({ @@ -756,13 +742,12 @@ function ReplyNoteList({
})
void (async () => {
const contextualReadRelays = threadProfileContextRelays
const chunks: string[][] = []
for (let i = 0; i < need.length; i += THREAD_PROFILE_CHUNK) {
chunks.push(need.slice(i, i + THREAD_PROFILE_CHUNK))
}
const settled = await Promise.allSettled(
chunks.map((chunk) => client.fetchProfilesForPubkeys(chunk, { contextualReadRelays }))
chunks.map((chunk) => client.fetchProfilesForPubkeys(chunk))
)
if (gen !== threadProfileBatchGenRef.current) return
@ -804,8 +789,7 @@ function ReplyNoteList({ @@ -804,8 +789,7 @@ function ReplyNoteList({
event,
mergedFeed,
parentNoteFeed?.profiles,
parentNoteFeed?.pendingPubkeys,
threadProfileContextRelays
parentNoteFeed?.pendingPubkeys
])
const [timelineKey] = useState<string | undefined>(undefined)

1
src/constants.ts

@ -411,7 +411,6 @@ export const READ_ONLY_RELAY_URLS = [ @@ -411,7 +411,6 @@ export const READ_ONLY_RELAY_URLS = [
'wss://relaypag.es',
'wss://relay.noswhere.com',
'wss://search.nos.today',
'wss://trending.nostr.wine',
'wss://relay.nip46.com',
'wss://filter.nostr.wine',
'wss://primus.nostr1.com'

17
src/lib/relay-list-builder.test.ts

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest'
import { pickAuthorNip65RelaysPreferringViewerOverlap } from './relay-list-builder'
describe('pickAuthorNip65RelaysPreferringViewerOverlap', () => {
it('prefers relays shared with the viewer, capped at max', () => {
const author = [
'wss://author-only.example/',
'wss://shared.example/',
'wss://author-two.example/'
]
const viewer = ['wss://shared.example/', 'wss://viewer-only.example/']
expect(pickAuthorNip65RelaysPreferringViewerOverlap(author, viewer, 2)).toEqual([
'wss://shared.example/',
'wss://author-only.example/'
])
})
})

202
src/lib/relay-list-builder.ts

@ -20,6 +20,46 @@ import client from '@/services/client.service' @@ -20,6 +20,46 @@ import client from '@/services/client.service'
import logger from '@/lib/logger'
import type { Event } from 'nostr-tools'
/** Max author NIP-65 read / write URLs merged into comprehensive read lists (shared with viewer first). */
export const AUTHOR_NIP65_RELAY_CAP = 2
function relayKey(url: string): string {
return (normalizeUrl(url) || normalizeAnyRelayUrl(url) || url.trim()).toLowerCase()
}
/**
* Up to `max` author relays, preferring URLs that also appear on the viewer's NIP-65 read/write lists.
*/
export function pickAuthorNip65RelaysPreferringViewerOverlap(
authorUrls: readonly string[],
viewerUrls: readonly string[],
max: number
): string[] {
if (max <= 0) return []
const viewerKeys = new Set(viewerUrls.map(relayKey).filter(Boolean))
const shared: string[] = []
const authorOnly: string[] = []
const seen = new Set<string>()
for (const raw of authorUrls) {
const k = relayKey(raw)
if (!k || seen.has(k)) continue
seen.add(k)
if (viewerKeys.has(k)) shared.push(normalizeAnyRelayUrl(raw) || raw.trim())
else authorOnly.push(normalizeAnyRelayUrl(raw) || raw.trim())
}
const out: string[] = []
const push = (u: string) => {
const k = relayKey(u)
if (!k || out.some((x) => relayKey(x) === k)) return
out.push(u)
}
for (const u of [...shared, ...authorOnly]) {
if (out.length >= max) break
push(u)
}
return out
}
function dedupeNormalizedRelayUrls(urls: string[]): string[] {
const seen = new Set<string>()
const out: string[] = []
@ -78,6 +118,13 @@ export interface RelayListBuilderOptions { @@ -78,6 +118,13 @@ export interface RelayListBuilderOptions {
* behind broken personal relays under the global connection cap.
*/
preferPublicReadRelaysEarly?: boolean
/**
* When set, skips {@link viewerUsesGlobalRelayDefaults} and forces fast-read bootstrap on/off.
* Otherwise fast-read is omitted for logged-in users who have favorites or NIP-65 configured.
*/
useGlobalRelayDefaults?: boolean
/** Append the viewer's kind 10243 HTTP index relays when present (not subject to WS-only `addRelay`). */
includeViewerHttpIndexRelays?: boolean
}
/**
@ -98,10 +145,13 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio @@ -98,10 +145,13 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio
blockedRelays = [],
includeLocalRelays = true,
includeFavoriteRelays = false,
preferPublicReadRelaysEarly = false
preferPublicReadRelaysEarly = false,
useGlobalRelayDefaults: useGlobalRelayDefaultsOption,
includeViewerHttpIndexRelays = true
} = options
const relayUrls = new Set<string>()
const httpRelayUrls: string[] = []
const normalizedBlocked = new Set(
(blockedRelays || []).map(url => {
const normalized = normalizeUrl(url) || url
@ -120,6 +170,49 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio @@ -120,6 +170,49 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio
relayUrls.add(normalized)
}
const addHttpRelay = (url: string | undefined) => {
if (!url || !isHttpRelayUrl(url)) return
const normalized = normalizeAnyRelayUrl(url) || url.trim()
if (!normalized || normalizedBlocked.has(normalized.toLowerCase())) return
if (httpRelayUrls.some((u) => relayKey(u) === relayKey(normalized))) return
httpRelayUrls.push(normalized)
}
let viewerRelayListForShare: { read?: string[]; write?: string[]; httpRead?: string[]; httpWrite?: string[] } | null =
null
if (userPubkey) {
try {
viewerRelayListForShare = await client.peekRelayListFromStorage(userPubkey)
} catch {
viewerRelayListForShare = null
}
}
const viewerWsForAuthorOverlap = [
...(viewerRelayListForShare?.read ?? []),
...(viewerRelayListForShare?.write ?? [])
]
let effectiveIncludeFastRead = includeFastReadRelays
if (userPubkey && includeFastReadRelays) {
if (useGlobalRelayDefaultsOption !== undefined) {
effectiveIncludeFastRead = useGlobalRelayDefaultsOption
} else {
try {
const fav =
includeFavoriteRelays && userPubkey
? await client.fetchFavoriteRelays(userPubkey).catch(() => [] as string[])
: []
effectiveIncludeFastRead = viewerUsesGlobalRelayDefaults({
viewerPubkey: userPubkey,
favoriteRelayUrls: fav,
relayList: viewerRelayListForShare ?? undefined
})
} catch {
effectiveIncludeFastRead = true
}
}
}
// 1. Relay hints (highest priority - explicit hints)
relayHints.forEach(addRelay)
@ -135,7 +228,7 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio @@ -135,7 +228,7 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio
if (includeProfileFetchRelays) {
PROFILE_RELAY_URLS.forEach(addRelay)
}
if (includeFastReadRelays) {
if (effectiveIncludeFastRead) {
FAST_READ_RELAY_URLS.forEach(addRelay)
}
if (includeSearchableRelays) {
@ -147,10 +240,16 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio @@ -147,10 +240,16 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio
if (authorPubkey) {
try {
const authorRelayList = await client.peekRelayListFromStorage(authorPubkey)
const authorOutboxes = [...(authorRelayList.write || []).slice(0, 10)]
authorOutboxes.forEach(addRelay)
const authorInboxes = userReadRelaysWithHttp(authorRelayList).slice(0, 10)
authorInboxes.forEach(addRelay)
pickAuthorNip65RelaysPreferringViewerOverlap(
authorRelayList.write ?? [],
viewerWsForAuthorOverlap,
AUTHOR_NIP65_RELAY_CAP
).forEach(addRelay)
pickAuthorNip65RelaysPreferringViewerOverlap(
authorRelayList.read ?? [],
viewerWsForAuthorOverlap,
AUTHOR_NIP65_RELAY_CAP
).forEach(addRelay)
} catch (error) {
logger.warn('[RelayListBuilder] Failed to read author relay list from storage', { error })
}
@ -159,7 +258,7 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio @@ -159,7 +258,7 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio
// 5. User's own relays (for profiles/metadata)
if (includeUserOwnRelays && userPubkey) {
try {
const userRelayList = await client.peekRelayListFromStorage(userPubkey)
const userRelayList = viewerRelayListForShare ?? (await client.peekRelayListFromStorage(userPubkey))
const userRead = userReadRelaysWithHttp(userRelayList).slice(0, 10)
const userWrite = [...(userRelayList.write || []).slice(0, 10)]
userRead.forEach(addRelay)
@ -186,10 +285,8 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio @@ -186,10 +285,8 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio
} else if (userPubkey) {
// Even if not including user's own relays, still include user's inboxes for reading
try {
const userRelayList = await client.peekRelayListFromStorage(userPubkey)
userReadRelaysWithHttp(userRelayList)
.slice(0, 10)
.forEach(addRelay)
const userRelayList = viewerRelayListForShare ?? (await client.peekRelayListFromStorage(userPubkey))
;(userRelayList.read ?? []).slice(0, 10).forEach(addRelay)
// Include local relays from kind 10432 if enabled
if (includeLocalRelays) {
@ -216,12 +313,12 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio @@ -216,12 +313,12 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio
}
// 7. Fast read relays (fallback)
if (includeFastReadRelays && !preferPublicReadRelaysEarly) {
if (effectiveIncludeFastRead && !preferPublicReadRelaysEarly) {
FAST_READ_RELAY_URLS.forEach(addRelay)
}
// 8. Extra fast-read bootstrap mirrors (call sites use legacy `includeFastWriteRelays`)
if (includeFastWriteRelays) {
if (includeFastWriteRelays && effectiveIncludeFastRead) {
FAST_READ_RELAY_URLS.forEach(addRelay)
}
@ -230,13 +327,89 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio @@ -230,13 +327,89 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio
SEARCHABLE_RELAY_URLS.forEach(addRelay)
}
if (includeViewerHttpIndexRelays && userPubkey && viewerRelayListForShare) {
const hasHttp =
(viewerRelayListForShare.httpRead?.length ?? 0) > 0 ||
(viewerRelayListForShare.httpWrite?.length ?? 0) > 0
if (hasHttp) {
;[...(viewerRelayListForShare.httpRead ?? []), ...(viewerRelayListForShare.httpWrite ?? [])].forEach(
addHttpRelay
)
}
}
const merged = Array.from(relayUrls)
return feedRelayPolicyUrls([{ source: 'fallback', urls: merged }], {
const ws = feedRelayPolicyUrls([{ source: 'fallback', urls: merged }], {
operation: 'read',
blockedRelays,
applySocialKindBlockedFilter: false,
allowThirdPartyLocalRelays: true
})
if (httpRelayUrls.length === 0) return ws
const seen = new Set(ws.map(relayKey))
const out = [...ws]
for (const u of httpRelayUrls) {
const k = relayKey(u)
if (!k || seen.has(k)) continue
seen.add(k)
out.push(u)
}
return out
}
/**
* Batched kind-0 / profile hydration: {@link PROFILE_RELAY_URLS} plus the logged-in viewer's own relays only.
*/
export async function buildProfileAndUserRelayList(
userPubkey: string | null | undefined,
blockedRelays: string[] = []
): Promise<string[]> {
const profileWs = dedupeNormalizedRelayUrls([...PROFILE_RELAY_URLS])
if (!userPubkey?.trim()) {
return feedRelayPolicyUrls([{ source: 'profile-fetch', urls: profileWs }], {
operation: 'read',
blockedRelays,
applySocialKindBlockedFilter: false,
allowThirdPartyLocalRelays: true
})
}
const userStack = await buildComprehensiveRelayList({
userPubkey,
includeUserOwnRelays: true,
includeProfileFetchRelays: false,
includeFastReadRelays: false,
includeFastWriteRelays: false,
includeSearchableRelays: false,
includeFavoriteRelays: true,
includeLocalRelays: true,
includeViewerHttpIndexRelays: true,
blockedRelays
})
const httpPart = userStack.filter((u) => isHttpRelayUrl(u))
const wsPart = userStack.filter((u) => !isHttpRelayUrl(u))
const seen = new Set<string>()
const mergedWs: string[] = []
for (const u of [...profileWs, ...wsPart]) {
const k = relayKey(u)
if (!k || seen.has(k)) continue
seen.add(k)
mergedWs.push(u)
}
const policyWs = feedRelayPolicyUrls([{ source: 'profile-fetch', urls: mergedWs }], {
operation: 'read',
blockedRelays,
applySocialKindBlockedFilter: false,
allowThirdPartyLocalRelays: true
})
const outSeen = new Set(policyWs.map(relayKey))
const out = [...policyWs]
for (const u of httpPart) {
const k = relayKey(u)
if (!k || outSeen.has(k)) continue
outSeen.add(k)
out.push(u)
}
return out
}
/**
@ -427,6 +600,7 @@ export async function buildReplyReadRelayList( @@ -427,6 +600,7 @@ export async function buildReplyReadRelayList(
relayHints: threadRelayHints,
includeUserOwnRelays: Boolean(userPubkey),
includeFastReadRelays: useGlobal,
useGlobalRelayDefaults: useGlobal,
includeSearchableRelays: false,
includeLocalRelays: true,
includeFavoriteRelays: Boolean(userPubkey),

115
src/services/client-replaceable-events.service.ts

@ -23,7 +23,11 @@ import indexedDb from './indexed-db.service' @@ -23,7 +23,11 @@ import indexedDb from './indexed-db.service'
import type { QueryService } from './client-query.service'
import logger from '@/lib/logger'
import client from './client.service'
import { buildComprehensiveRelayList, buildExploreProfileAndUserRelayList } from '@/lib/relay-list-builder'
import {
buildComprehensiveRelayList,
buildExploreProfileAndUserRelayList,
buildProfileAndUserRelayList
} from '@/lib/relay-list-builder'
import { prependAggrNostrLandIfViewerEligible } from '@/lib/nostr-land-relay-eligibility'
import { stripLocalNetworkRelaysForWssReq } from '@/lib/relay-list-sanitize'
import { shouldDropEventOnIngest } from '@/lib/event-ingest-filter'
@ -128,16 +132,28 @@ export class ReplaceableEventService { @@ -128,16 +132,28 @@ export class ReplaceableEventService {
containingEventRelays: string[] = []
): Promise<string[]> {
const userPubkey = client.pubkey
const isProfileOrMetadata = kind === kinds.Metadata || kind === kinds.RelayList
if (kind === kinds.Metadata) {
const profileStack = await buildProfileAndUserRelayList(userPubkey)
const hintLayer = [...relayHints, ...containingEventRelays]
if (hintLayer.length === 0) return profileStack
return Array.from(
new Set([
...profileStack,
...hintLayer
.map((u) => normalizeAnyRelayUrl(u) || normalizeUrl(u) || u.trim())
.filter((u): u is string => !!u && !isHttpRelayUrl(u))
])
)
}
// Use the comprehensive relay list builder
const isProfileOrMetadata = kind === kinds.RelayList
return buildComprehensiveRelayList({
authorPubkey,
userPubkey,
relayHints,
containingEventRelays,
includeUserOwnRelays: isProfileOrMetadata, // For profiles/metadata, include user's own relays
includeProfileFetchRelays: isProfileOrMetadata, // For profiles/metadata, include PROFILE_RELAY_URLS
includeUserOwnRelays: isProfileOrMetadata,
includeProfileFetchRelays: isProfileOrMetadata,
includeFastReadRelays: true,
includeLocalRelays: true
})
@ -529,26 +545,10 @@ export class ReplaceableEventService { @@ -529,26 +545,10 @@ export class ReplaceableEventService {
// (profile + FAST_READ + viewer read/write/local when logged in).
let relayUrls: string[]
if (kind === kinds.Metadata) {
const userPk = client.pubkey
if (userPk) {
try {
relayUrls = await buildComprehensiveRelayList({
userPubkey: userPk,
includeUserOwnRelays: false,
includeProfileFetchRelays: true,
includeFastReadRelays: true,
includeFavoriteRelays: true,
includeLocalRelays: true,
/** Many users publish kind 0 to NIP-65 write relays; batch path includes public read mirrors via {@link buildComprehensiveRelayList}. */
includeFastWriteRelays: false,
includeSearchableRelays: false,
preferPublicReadRelaysEarly: true
})
} catch {
relayUrls = Array.from(new Set([...PROFILE_RELAY_URLS, ...FAST_READ_RELAY_URLS]))
}
} else {
relayUrls = Array.from(new Set([...PROFILE_RELAY_URLS, ...FAST_READ_RELAY_URLS]))
try {
relayUrls = await buildProfileAndUserRelayList(client.pubkey)
} catch {
relayUrls = [...PROFILE_RELAY_URLS]
}
} else if (kind === ExtendedKind.FAVORITE_RELAYS) {
relayUrls = await buildExploreProfileAndUserRelayList(client.pubkey)
@ -1047,20 +1047,13 @@ export class ReplaceableEventService { @@ -1047,20 +1047,13 @@ export class ReplaceableEventService {
return getProfileFromEvent(event)
}
/**
* Fetch profiles for multiple pubkeys
* @param contextualReadRelays Optional relays used for the surrounding feed/thread REQ queried for kind-0
* when default profile mirrors miss (e.g. metadata only on a community relay).
*/
async fetchProfilesForPubkeys(
pubkeys: string[],
options?: { contextualReadRelays?: string[] }
): Promise<TProfile[]> {
/** Fetch profiles for multiple pubkeys (profile mirrors + viewer's own relays only). */
async fetchProfilesForPubkeys(pubkeys: string[]): Promise<TProfile[]> {
const deduped = Array.from(new Set(pubkeys.filter((p) => p && p.length === 64)))
if (deduped.length === 0) return []
try {
return await racePromiseWithTimeout(
this.fetchProfilesForPubkeysBody(deduped, options),
this.fetchProfilesForPubkeysBody(deduped),
FEED_PROFILE_BATCH_FETCH_TIMEOUT_MS,
'fetchProfilesForPubkeys'
)
@ -1121,10 +1114,7 @@ export class ReplaceableEventService { @@ -1121,10 +1114,7 @@ export class ReplaceableEventService {
return profiles
}
private async fetchProfilesForPubkeysBody(
deduped: string[],
options?: { contextualReadRelays?: string[] }
): Promise<TProfile[]> {
private async fetchProfilesForPubkeysBody(deduped: string[]): Promise<TProfile[]> {
let events = await this.fetchReplaceableEventsFromProfileFetchRelays(deduped, kinds.Metadata)
const gapIdx: number[] = []
for (let i = 0; i < deduped.length; i++) {
@ -1177,53 +1167,6 @@ export class ReplaceableEventService { @@ -1177,53 +1167,6 @@ export class ReplaceableEventService {
)
}
const stillMissingIdx: number[] = []
for (let i = 0; i < deduped.length; i++) {
if (!events[i]) stillMissingIdx.push(i)
}
if (stillMissingIdx.length > 0 && options?.contextualReadRelays?.length) {
const urls = Array.from(
new Set(
options.contextualReadRelays
.map((u) => normalizeAnyRelayUrl(u) || normalizeUrl(u) || u.trim())
.filter((u): u is string => !!u && !isHttpRelayUrl(u))
)
)
if (urls.length > 0) {
const authors = stillMissingIdx.map((i) => deduped[i]!)
try {
const sanitizedUrls = stripLocalNetworkRelaysForWssReq(urls)
const withAggr = prependAggrNostrLandIfViewerEligible(sanitizedUrls)
if (withAggr.length > 0) {
const evs = await this.queryService.query(
withAggr,
{
kinds: [kinds.Metadata],
authors,
limit: Math.min(Math.max(authors.length * 2, authors.length), 500)
} as Filter,
undefined,
{
firstRelayResultGraceMs: false,
globalTimeout: METADATA_BATCH_QUERY_GLOBAL_TIMEOUT_MS,
eoseTimeout: METADATA_BATCH_QUERY_EOSE_TIMEOUT_MS,
replaceableRace: false
}
)
for (const ev of evs) {
if (ev.kind !== kinds.Metadata || shouldDropEventOnIngest(ev)) continue
const ix = deduped.findIndex((p) => p.toLowerCase() === ev.pubkey.toLowerCase())
if (ix >= 0 && !events[ix]) {
events[ix] = ev
}
}
}
} catch {
/* best-effort */
}
}
}
return this.profilesFromMetadataEvents(deduped, events)
}

7
src/services/client.service.ts

@ -4094,11 +4094,8 @@ class ClientService extends EventTarget { @@ -4094,11 +4094,8 @@ class ClientService extends EventTarget {
return this.replaceableEventService.fetchProfile(id, skipCache)
}
async fetchProfilesForPubkeys(
pubkeys: string[],
options?: { contextualReadRelays?: string[] }
): Promise<TProfile[]> {
return this.replaceableEventService.fetchProfilesForPubkeys(pubkeys, options)
async fetchProfilesForPubkeys(pubkeys: string[]): Promise<TProfile[]> {
return this.replaceableEventService.fetchProfilesForPubkeys(pubkeys)
}
async getProfileFromIndexedDB(id: string): Promise<TProfile | undefined> {

123
src/services/note-stats.service.ts

@ -1,9 +1,4 @@ @@ -1,9 +1,4 @@
import {
ExtendedKind,
FAST_READ_RELAY_URLS,
NOTE_STATS_OP_REFERENCE_KINDS_WITHOUT_HIGHLIGHT,
SEARCHABLE_RELAY_URLS
} from '@/constants'
import { ExtendedKind, NOTE_STATS_OP_REFERENCE_KINDS_WITHOUT_HIGHLIGHT } from '@/constants'
import { replaceStandardEmojiShortcodesInContent } from '@/lib/emoji-content'
import {
getNip18RepostTargetId,
@ -27,16 +22,15 @@ import { @@ -27,16 +22,15 @@ import {
} from '@/lib/rss-article'
import { eventReferencesThreadTarget, threadRootRefFromStatsRootEvent } from '@/lib/op-reference-tags'
import type { TThreadRootRef } from '@/lib/thread-reply-root-match'
import { feedRelayPolicyUrls } from '@/features/feed/relay-policy'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { buildComprehensiveRelayList, relayHintsFromEventTags } from '@/lib/relay-list-builder'
import { viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults'
import {
getEmojiInfosFromEmojiTags,
getNip25ReactionTargetHexFromTags,
tagNameEquals
} from '@/lib/tag'
import { normalizeAnyRelayUrl } from '@/lib/url'
import client, { eventService } from '@/services/client.service'
import { TEmoji, type TRelayList } from '@/types'
import { TEmoji } from '@/types'
import dayjs from 'dayjs'
import { Event, Filter, kinds } from 'nostr-tools'
@ -585,77 +579,45 @@ class NoteStatsService { @@ -585,77 +579,45 @@ class NoteStatsService {
}
}
/**
* Build relay list for note stats: SEARCHABLE + FAST_READ + optional user favorites + seen relays +
* `e`-tag hints on the note + hints from session-cached referrers + author NIP-65 read (slice 10).
*/
/** {@link buildComprehensiveRelayList} for reactions/reposts/zaps on a note (thread hints, capped author NIP-65). */
private async buildNoteStatsRelayList(event: Event, favoriteRelays?: string[] | null): Promise<string[]> {
const seen = new Set<string>()
const add = (url: string | undefined) => {
if (!url) return
// Must use normalizeAnyRelayUrl, not normalizeUrl: the latter converts http(s)://
// index relay URLs into ws(s):// which then hit the WebSocket pool.
const n = normalizeAnyRelayUrl(url)
if (n) seen.add(n)
}
// 1. Search / discovery relay set (includes read-only index mirrors; see READ_ONLY_RELAY_URLS in constants)
SEARCHABLE_RELAY_URLS.forEach(add)
// 2. Default fast read set (includes e.g. theforest — not in SEARCHABLE)
FAST_READ_RELAY_URLS.forEach(add)
// 3. User's favorite relays (spell feed / sidebar) — was previously ignored
favoriteRelays?.forEach(add)
// 4. Relay(s) where the event was seen
client.getSeenEventRelayUrls(event.id).forEach(add)
const me = client.pubkey?.trim()
const relayHints = [
...relayHintsFromEventTags(event),
...client.getSeenEventRelayUrls(event.id),
...client.eventService.getSessionRelayHintsForHexTarget(event.id),
...(favoriteRelays ?? [])
]
// 5. NIP-10 `e`-tag relay hints on the note itself (often where replies/reactions to it were published)
for (const t of event.tags) {
if ((t[0] === 'e' || t[0] === 'E') && t[2]?.trim()) {
add(t[2])
let useGlobal = true
if (me) {
try {
const [fav, rl] = await Promise.all([
client.fetchFavoriteRelays(me).catch(() => [] as string[]),
client.peekRelayListFromStorage(me)
])
useGlobal = viewerUsesGlobalRelayDefaults({
viewerPubkey: me,
favoriteRelayUrls: fav,
relayList: rl ?? undefined
})
} catch {
useGlobal = true
}
}
// 6. Session cache (e.g. notifications): events that reference this id with a relay hint
client.eventService.getSessionRelayHintsForHexTarget(event.id).forEach(add)
const emptyViewerRl: TRelayList = {
write: [],
read: [],
originalRelays: [],
httpRead: [],
httpWrite: [],
httpOriginalRelays: []
}
const me = client.pubkey?.trim()
const [authorRelayList, viewerRelayList] = await Promise.all([
Promise.race([
client.fetchRelayList(event.pubkey),
new Promise<{ read?: string[] }>((r) => setTimeout(() => r({}), 1500))
]).catch(() => undefined),
me
? Promise.race([
client.fetchRelayList(me),
new Promise<TRelayList>((r) => setTimeout(() => r(emptyViewerRl), 1500))
]).catch(() => undefined)
: Promise.resolve(undefined)
])
// 7. Author's inboxes (read relays from kind 10002)
if (authorRelayList) {
userReadRelaysWithHttp(authorRelayList).slice(0, 10).forEach(add)
}
// 8. Logged-in viewer's inboxes (NIP-65 read + kind 10243 http read) — same events often land on personal relays.
if (viewerRelayList) {
userReadRelaysWithHttp(viewerRelayList).slice(0, 12).forEach(add)
}
return feedRelayPolicyUrls([{ source: 'fallback', urls: Array.from(seen) }], {
operation: 'read',
applySocialKindBlockedFilter: false,
allowThirdPartyLocalRelays: true
return buildComprehensiveRelayList({
authorPubkey: event.pubkey,
userPubkey: me,
relayHints,
includeUserOwnRelays: Boolean(me),
includeFavoriteRelays: Boolean(me),
includeFastReadRelays: useGlobal,
useGlobalRelayDefaults: useGlobal,
includeProfileFetchRelays: false,
includeSearchableRelays: false,
includeLocalRelays: true,
includeViewerHttpIndexRelays: true
})
}
@ -1036,14 +998,7 @@ class NoteStatsService { @@ -1036,14 +998,7 @@ class NoteStatsService {
if (!/^[0-9a-f]{64}$/i.test(rootHex)) return
const hintRelays = client.eventService.getSessionRelayHintsForHexTarget(rootHex)
const urls = feedRelayPolicyUrls(
[{ source: 'fallback', urls: [...new Set([...hintRelays, ...relayUrls])] }],
{
operation: 'read',
applySocialKindBlockedFilter: false,
allowThirdPartyLocalRelays: true
}
)
const urls = [...new Set([...relayUrls, ...hintRelays])]
if (!urls.length) return
const filters: Filter[] = [

Loading…
Cancel
Save