feat: track event relays

This commit is contained in:
codytseng
2025-02-06 14:43:27 +08:00
parent 6ffa180812
commit 372e7a9976
4 changed files with 52 additions and 13 deletions

View File

@@ -1,5 +1,6 @@
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { BIG_RELAY_URLS, COMMENT_EVENT_KIND } from '@/constants' import { BIG_RELAY_URLS, COMMENT_EVENT_KIND } from '@/constants'
import { isCommentEvent, isProtectedEvent } from '@/lib/event'
import { tagNameEquals } from '@/lib/tag' import { tagNameEquals } from '@/lib/tag'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
@@ -10,7 +11,6 @@ import { Event as NEvent } from 'nostr-tools'
import { useCallback, useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import ReplyNote from '../ReplyNote' import ReplyNote from '../ReplyNote'
import { isCommentEvent } from '@/lib/event'
const LIMIT = 100 const LIMIT = 100
@@ -62,8 +62,13 @@ export default function Nip22ReplyNoteList({
try { try {
const relayList = await client.fetchRelayList(event.pubkey) const relayList = await client.fetchRelayList(event.pubkey)
const relayUrls = relayList.read.concat(BIG_RELAY_URLS)
if (isProtectedEvent(event)) {
const seenOn = client.getSeenEventRelayUrls(event.id)
relayUrls.unshift(...seenOn)
}
const { closer, timelineKey } = await client.subscribeTimeline( const { closer, timelineKey } = await client.subscribeTimeline(
relayList.read.concat(BIG_RELAY_URLS).slice(0, 4), relayUrls.slice(0, 4),
{ {
'#E': [event.id], '#E': [event.id],
kinds: [COMMENT_EVENT_KIND], kinds: [COMMENT_EVENT_KIND],

View File

@@ -1,5 +1,6 @@
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { isReplyNoteEvent } from '@/lib/event' import { BIG_RELAY_URLS } from '@/constants'
import { isProtectedEvent, isReplyNoteEvent } from '@/lib/event'
import { isReplyETag, isRootETag } from '@/lib/tag' import { isReplyETag, isRootETag } from '@/lib/tag'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
@@ -10,7 +11,6 @@ import { Event as NEvent, kinds } from 'nostr-tools'
import { useCallback, useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import ReplyNote from '../ReplyNote' import ReplyNote from '../ReplyNote'
import { BIG_RELAY_URLS } from '@/constants'
const LIMIT = 100 const LIMIT = 100
@@ -56,8 +56,13 @@ export default function ReplyNoteList({ event, className }: { event: NEvent; cla
try { try {
const relayList = await client.fetchRelayList(event.pubkey) const relayList = await client.fetchRelayList(event.pubkey)
const relayUrls = relayList.read.concat(BIG_RELAY_URLS)
if (isProtectedEvent(event)) {
const seenOn = client.getSeenEventRelayUrls(event.id)
relayUrls.unshift(...seenOn)
}
const { closer, timelineKey } = await client.subscribeTimeline( const { closer, timelineKey } = await client.subscribeTimeline(
relayList.read.concat(BIG_RELAY_URLS).slice(0, 4), relayUrls.slice(0, 4),
{ {
'#e': [event.id], '#e': [event.id],
kinds: [kinds.ShortTextNote], kinds: [kinds.ShortTextNote],

View File

@@ -41,6 +41,10 @@ export function isPictureEvent(event: Event) {
return event.kind === PICTURE_EVENT_KIND return event.kind === PICTURE_EVENT_KIND
} }
export function isProtectedEvent(event: Event) {
return event.tags.some(([tagName]) => tagName === '-')
}
export function getParentEventId(event?: Event) { export function getParentEventId(event?: Event) {
if (!event || !isReplyNoteEvent(event)) return undefined if (!event || !isReplyNoteEvent(event)) return undefined
return event.tags.find(isReplyETag)?.[1] ?? event.tags.find(tagNameEquals('e'))?.[1] return event.tags.find(isReplyETag)?.[1] ?? event.tags.find(tagNameEquals('e'))?.[1]

View File

@@ -16,6 +16,7 @@ import {
SimplePool, SimplePool,
VerifiedEvent VerifiedEvent
} from 'nostr-tools' } from 'nostr-tools'
import { AbstractRelay } from 'nostr-tools/abstract-relay'
type TTimelineRef = [string, number] type TTimelineRef = [string, number]
@@ -23,7 +24,7 @@ class ClientService extends EventTarget {
static instance: ClientService static instance: ClientService
private defaultRelayUrls: string[] = BIG_RELAY_URLS private defaultRelayUrls: string[] = BIG_RELAY_URLS
private pool = new SimplePool() private pool: SimplePool
private timelines: Record< private timelines: Record<
string, string,
@@ -84,6 +85,8 @@ class ClientService extends EventTarget {
constructor() { constructor() {
super() super()
this.pool = new SimplePool()
this.pool.trackRelays = true
} }
public static getInstance(): ClientService { public static getInstance(): ClientService {
@@ -117,13 +120,21 @@ class ClientService extends EventTarget {
const result = await Promise.any( const result = await Promise.any(
relayUrls.map(async (url) => { relayUrls.map(async (url) => {
const relay = await this.pool.ensureRelay(url) const relay = await this.pool.ensureRelay(url)
return relay.publish(event).catch((error) => { return relay
if (error instanceof Error && error.message.startsWith('auth-required:') && signer) { .publish(event)
relay.auth((authEvt: EventTemplate) => signer(authEvt)).then(() => relay.publish(event)) .then((reason) => {
} else { this.trackEventSeenOn(event.id, relay)
throw error return reason
} })
}) .catch((error) => {
if (error instanceof Error && error.message.startsWith('auth-required:') && signer) {
relay
.auth((authEvt: EventTemplate) => signer(authEvt))
.then(() => relay.publish(event))
} else {
throw error
}
})
}) })
) )
this.dispatchEvent(new CustomEvent('eventPublished', { detail: event })) this.dispatchEvent(new CustomEvent('eventPublished', { detail: event }))
@@ -203,6 +214,7 @@ class ClientService extends EventTarget {
}, },
onevent: (evt: NEvent) => { onevent: (evt: NEvent) => {
that.eventDataLoader.prime(evt.id, Promise.resolve(evt)) that.eventDataLoader.prime(evt.id, Promise.resolve(evt))
that.trackEventSeenOn(evt.id, relay)
// not eosed yet, push to events // not eosed yet, push to events
if (eosedCount < startedCount) { if (eosedCount < startedCount) {
return events.push(evt) return events.push(evt)
@@ -537,6 +549,10 @@ class ClientService extends EventTarget {
} }
} }
getSeenEventRelayUrls(eventId: string) {
return Array.from(this.pool.seenOn.get(eventId)?.values() || []).map((relay) => relay.url)
}
private async fetchEventById(relayUrls: string[], id: string): Promise<NEvent | undefined> { private async fetchEventById(relayUrls: string[], id: string): Promise<NEvent | undefined> {
const event = await this.fetchEventFromDefaultRelaysDataloader.load(id) const event = await this.fetchEventFromDefaultRelaysDataloader.load(id)
if (event) { if (event) {
@@ -740,6 +756,15 @@ class ClientService extends EventTarget {
return followListEvents.sort((a, b) => b.created_at - a.created_at)[0] return followListEvents.sort((a, b) => b.created_at - a.created_at)[0]
} }
private trackEventSeenOn(eventId: string, relay: AbstractRelay) {
let set = this.pool.seenOn.get(eventId)
if (!set) {
set = new Set()
this.pool.seenOn.set(eventId, set)
}
set.add(relay)
}
} }
const instance = ClientService.getInstance() const instance = ClientService.getInstance()