Browse Source

feat: optimize for algo relays

imwald
codytseng 1 year ago
parent
commit
9dec5201ba
  1. 36
      src/renderer/src/components/NoteList/index.tsx
  2. 4
      src/renderer/src/components/RelaySettings/RelayUrl.tsx
  3. 2
      src/renderer/src/components/RelaySettings/TemporaryRelayGroup.tsx
  4. 13
      src/renderer/src/hooks/useFetchRelayInfos.tsx
  5. 5
      src/renderer/src/lib/relay.ts
  6. 6
      src/renderer/src/providers/NostrProvider.tsx
  7. 19
      src/renderer/src/providers/RelaySettingsProvider.tsx
  8. 49
      src/renderer/src/services/client.service.ts
  9. 7
      src/renderer/src/services/storage.service.ts
  10. 1
      src/renderer/src/types.ts

36
src/renderer/src/components/NoteList/index.tsx

@ -1,4 +1,5 @@
import { Button } from '@renderer/components/ui/button' import { Button } from '@renderer/components/ui/button'
import { useFetchRelayInfos } from '@renderer/hooks'
import { isReplyNoteEvent } from '@renderer/lib/event' import { isReplyNoteEvent } from '@renderer/lib/event'
import { cn } from '@renderer/lib/utils' import { cn } from '@renderer/lib/utils'
import { useNostr } from '@renderer/providers/NostrProvider' import { useNostr } from '@renderer/providers/NostrProvider'
@ -6,8 +7,8 @@ import client from '@renderer/services/client.service'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { Event, Filter, kinds } from 'nostr-tools' import { Event, Filter, kinds } from 'nostr-tools'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import NoteCard from '../NoteCard'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import NoteCard from '../NoteCard'
export default function NoteList({ export default function NoteList({
relayUrls, relayUrls,
@ -19,7 +20,8 @@ export default function NoteList({
className?: string className?: string
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { isReady, singEvent } = useNostr() const { isReady, signEvent } = useNostr()
const { isFetching: isFetchingRelayInfo, areAlgoRelays } = useFetchRelayInfos(relayUrls)
const [events, setEvents] = useState<Event[]>([]) const [events, setEvents] = useState<Event[]>([])
const [newEvents, setNewEvents] = useState<Event[]>([]) const [newEvents, setNewEvents] = useState<Event[]>([])
const [until, setUntil] = useState<number>(() => dayjs().unix()) const [until, setUntil] = useState<number>(() => dayjs().unix())
@ -30,13 +32,13 @@ export default function NoteList({
const noteFilter = useMemo(() => { const noteFilter = useMemo(() => {
return { return {
kinds: [kinds.ShortTextNote, kinds.Repost], kinds: [kinds.ShortTextNote, kinds.Repost],
limit: 200, limit: areAlgoRelays ? 500 : 200,
...filter ...filter
} }
}, [JSON.stringify(filter)]) }, [JSON.stringify(filter), areAlgoRelays])
useEffect(() => { useEffect(() => {
if (!isReady) return if (!isReady || isFetchingRelayInfo) return
setInitialized(false) setInitialized(false)
setEvents([]) setEvents([])
@ -48,6 +50,9 @@ export default function NoteList({
noteFilter, noteFilter,
{ {
onEose: (events) => { onEose: (events) => {
if (!areAlgoRelays) {
events.sort((a, b) => b.created_at - a.created_at)
}
const processedEvents = events.filter((e) => !isReplyNoteEvent(e)) const processedEvents = events.filter((e) => !isReplyNoteEvent(e))
if (processedEvents.length > 0) { if (processedEvents.length > 0) {
setEvents((pre) => [...pre, ...processedEvents]) setEvents((pre) => [...pre, ...processedEvents])
@ -55,6 +60,9 @@ export default function NoteList({
if (events.length > 0) { if (events.length > 0) {
setUntil(events[events.length - 1].created_at - 1) setUntil(events[events.length - 1].created_at - 1)
} }
if (areAlgoRelays) {
setHasMore(false)
}
setInitialized(true) setInitialized(true)
}, },
onNew: (event) => { onNew: (event) => {
@ -63,13 +71,19 @@ export default function NoteList({
} }
} }
}, },
singEvent signEvent
) )
return () => { return () => {
subCloser() subCloser()
} }
}, [JSON.stringify(relayUrls), JSON.stringify(noteFilter), isReady]) }, [
JSON.stringify(relayUrls),
JSON.stringify(noteFilter),
isReady,
isFetchingRelayInfo,
areAlgoRelays
])
useEffect(() => { useEffect(() => {
if (!initialized) return if (!initialized) return
@ -119,9 +133,9 @@ export default function NoteList({
} }
return ( return (
<> <div className="space-y-2 sm:space-y-4">
{newEvents.length > 0 && ( {newEvents.length > 0 && (
<div className="flex justify-center w-full sm:mb-4 max-sm:mt-2"> <div className="flex justify-center w-full max-sm:mt-2">
<Button size="lg" onClick={showNewEvents}> <Button size="lg" onClick={showNewEvents}>
{t('show new notes')} {t('show new notes')}
</Button> </Button>
@ -132,9 +146,9 @@ export default function NoteList({
<NoteCard key={`${i}-${event.id}`} className="w-full" event={event} /> <NoteCard key={`${i}-${event.id}`} className="w-full" event={event} />
))} ))}
</div> </div>
<div className="text-center text-sm text-muted-foreground mt-2"> <div className="text-center text-sm text-muted-foreground">
{hasMore ? <div ref={bottomRef}>{t('loading...')}</div> : t('no more notes')} {hasMore ? <div ref={bottomRef}>{t('loading...')}</div> : t('no more notes')}
</div> </div>
</> </div>
) )
} }

4
src/renderer/src/components/RelaySettings/RelayUrl.tsx

@ -113,7 +113,9 @@ function RelayUrl({
onRemove: () => void onRemove: () => void
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const [relayInfo] = useFetchRelayInfos([url]) const {
relayInfos: [relayInfo]
} = useFetchRelayInfos([url])
return ( return (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">

2
src/renderer/src/components/RelaySettings/TemporaryRelayGroup.tsx

@ -15,7 +15,7 @@ export default function TemporaryRelayGroup() {
isConnected: boolean isConnected: boolean
}[] }[]
>(temporaryRelayUrls.map((url) => ({ url, isConnected: false }))) >(temporaryRelayUrls.map((url) => ({ url, isConnected: false })))
const relayInfos = useFetchRelayInfos(relays.map((relay) => relay.url)) const { relayInfos } = useFetchRelayInfos(relays.map((relay) => relay.url))
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {

13
src/renderer/src/hooks/useFetchRelayInfos.tsx

@ -1,23 +1,32 @@
import { checkIfAlgoRelay } from '@renderer/lib/relay'
import client from '@renderer/services/client.service' import client from '@renderer/services/client.service'
import { TRelayInfo } from '@renderer/types' import { TRelayInfo } from '@renderer/types'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
export function useFetchRelayInfos(urls: string[]) { export function useFetchRelayInfos(urls: string[]) {
const [isFetching, setIsFetching] = useState(true)
const [relayInfos, setRelayInfos] = useState<(TRelayInfo | undefined)[]>([]) const [relayInfos, setRelayInfos] = useState<(TRelayInfo | undefined)[]>([])
const [areAlgoRelays, setAreAlgoRelays] = useState(false)
useEffect(() => { useEffect(() => {
const fetchRelayInfos = async () => { const fetchRelayInfos = async () => {
if (urls.length === 0) return setIsFetching(true)
if (urls.length === 0) {
return setIsFetching(false)
}
try { try {
const relayInfos = await client.fetchRelayInfos(urls) const relayInfos = await client.fetchRelayInfos(urls)
setRelayInfos(relayInfos) setRelayInfos(relayInfos)
setAreAlgoRelays(relayInfos.every((relayInfo) => checkIfAlgoRelay(relayInfo)))
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} finally {
setIsFetching(false)
} }
} }
fetchRelayInfos() fetchRelayInfos()
}, [JSON.stringify(urls)]) }, [JSON.stringify(urls)])
return relayInfos return { relayInfos, isFetching, areAlgoRelays }
} }

5
src/renderer/src/lib/relay.ts

@ -0,0 +1,5 @@
import { TRelayInfo } from '@renderer/types'
export function checkIfAlgoRelay(relayInfo: TRelayInfo | undefined) {
return relayInfo?.software === 'https://github.com/bitvora/algo-relay' // hardcode for now
}

6
src/renderer/src/providers/NostrProvider.tsx

@ -21,7 +21,7 @@ type TNostrContext = {
*/ */
publish: (draftEvent: TDraftEvent, additionalRelayUrls?: string[]) => Promise<Event> publish: (draftEvent: TDraftEvent, additionalRelayUrls?: string[]) => Promise<Event>
signHttpAuth: (url: string, method: string) => Promise<string> signHttpAuth: (url: string, method: string) => Promise<string>
singEvent: (draftEvent: TDraftEvent) => Promise<Event> signEvent: (draftEvent: TDraftEvent) => Promise<Event>
checkLogin: (cb?: () => void | Promise<void>) => void checkLogin: (cb?: () => void | Promise<void>) => void
} }
@ -117,7 +117,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
return event return event
} }
const singEvent = async (draftEvent: TDraftEvent) => { const signEvent = async (draftEvent: TDraftEvent) => {
const event = await window.nostr?.signEvent(draftEvent) const event = await window.nostr?.signEvent(draftEvent)
if (!event) { if (!event) {
throw new Error('sign event failed') throw new Error('sign event failed')
@ -173,7 +173,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
publish, publish,
signHttpAuth, signHttpAuth,
checkLogin, checkLogin,
singEvent signEvent
}} }}
> >
{children} {children}

19
src/renderer/src/providers/RelaySettingsProvider.tsx

@ -1,4 +1,5 @@
import { TRelayGroup } from '@common/types' import { TRelayGroup } from '@common/types'
import { checkIfAlgoRelay } from '@renderer/lib/relay'
import { isWebsocketUrl, normalizeUrl } from '@renderer/lib/url' import { isWebsocketUrl, normalizeUrl } from '@renderer/lib/url'
import client from '@renderer/services/client.service' import client from '@renderer/services/client.service'
import storage from '@renderer/services/storage.service' import storage from '@renderer/services/storage.service'
@ -9,6 +10,7 @@ type TRelaySettingsContext = {
temporaryRelayUrls: string[] temporaryRelayUrls: string[]
relayUrls: string[] relayUrls: string[]
searchableRelayUrls: string[] searchableRelayUrls: string[]
areAlgoRelays: boolean
switchRelayGroup: (groupName: string) => void switchRelayGroup: (groupName: string) => void
renameRelayGroup: (oldGroupName: string, newGroupName: string) => string | null renameRelayGroup: (oldGroupName: string, newGroupName: string) => string | null
deleteRelayGroup: (groupName: string) => void deleteRelayGroup: (groupName: string) => void
@ -36,6 +38,7 @@ export function RelaySettingsProvider({ children }: { children: React.ReactNode
: (relayGroups.find((group) => group.isActive)?.relayUrls ?? []) : (relayGroups.find((group) => group.isActive)?.relayUrls ?? [])
) )
const [searchableRelayUrls, setSearchableRelayUrls] = useState<string[]>([]) const [searchableRelayUrls, setSearchableRelayUrls] = useState<string[]>([])
const [areAlgoRelays, setAreAlgoRelays] = useState(false)
useEffect(() => { useEffect(() => {
const init = async () => { const init = async () => {
@ -55,23 +58,24 @@ export function RelaySettingsProvider({ children }: { children: React.ReactNode
}, []) }, [])
useEffect(() => { useEffect(() => {
setRelayUrls( const handler = async () => {
temporaryRelayUrls.length const relayUrls = temporaryRelayUrls.length
? temporaryRelayUrls ? temporaryRelayUrls
: (relayGroups.find((group) => group.isActive)?.relayUrls ?? []) : (relayGroups.find((group) => group.isActive)?.relayUrls ?? [])
)
}, [relayGroups, temporaryRelayUrls])
useEffect(() => {
const handler = async () => {
setSearchableRelayUrls([]) setSearchableRelayUrls([])
setRelayUrls([])
const relayInfos = await client.fetchRelayInfos(relayUrls) const relayInfos = await client.fetchRelayInfos(relayUrls)
setSearchableRelayUrls( setSearchableRelayUrls(
relayUrls.filter((_, index) => relayInfos[index]?.supported_nips?.includes(50)) relayUrls.filter((_, index) => relayInfos[index]?.supported_nips?.includes(50))
) )
const nonAlgoRelayUrls = relayUrls.filter((_, index) => !checkIfAlgoRelay(relayInfos[index]))
setAreAlgoRelays(relayUrls.length > 0 && nonAlgoRelayUrls.length === 0)
setRelayUrls(relayUrls)
client.setCurrentRelayUrls(nonAlgoRelayUrls)
} }
handler() handler()
}, [relayUrls]) }, [relayGroups, temporaryRelayUrls])
const updateGroups = async (fn: (pre: TRelayGroup[]) => TRelayGroup[]) => { const updateGroups = async (fn: (pre: TRelayGroup[]) => TRelayGroup[]) => {
let newGroups = relayGroups let newGroups = relayGroups
@ -154,6 +158,7 @@ export function RelaySettingsProvider({ children }: { children: React.ReactNode
temporaryRelayUrls, temporaryRelayUrls,
relayUrls, relayUrls,
searchableRelayUrls, searchableRelayUrls,
areAlgoRelays,
switchRelayGroup, switchRelayGroup,
renameRelayGroup, renameRelayGroup,
deleteRelayGroup, deleteRelayGroup,

49
src/renderer/src/services/client.service.ts

@ -26,6 +26,7 @@ const BIG_RELAY_URLS = [
class ClientService { class ClientService {
static instance: ClientService static instance: ClientService
private defaultRelayUrls: string[] = BIG_RELAY_URLS
private pool = new SimplePool() private pool = new SimplePool()
private eventCache = new LRUCache<string, Promise<NEvent | undefined>>({ max: 10000 }) private eventCache = new LRUCache<string, Promise<NEvent | undefined>>({ max: 10000 })
@ -33,7 +34,7 @@ class ClientService {
(ids) => Promise.all(ids.map((id) => this._fetchEvent(id))), (ids) => Promise.all(ids.map((id) => this._fetchEvent(id))),
{ cacheMap: this.eventCache } { cacheMap: this.eventCache }
) )
private fetchEventFromBigRelaysDataloader = new DataLoader<string, NEvent | undefined>( private fetchEventFromDefaultRelaysDataloader = new DataLoader<string, NEvent | undefined>(
this.eventBatchLoadFn.bind(this), this.eventBatchLoadFn.bind(this),
{ cache: false } { cache: false }
) )
@ -45,7 +46,7 @@ class ClientService {
(ids) => Promise.all(ids.map((id) => this._fetchProfile(id))), (ids) => Promise.all(ids.map((id) => this._fetchProfile(id))),
{ cacheMap: this.profileCache } { cacheMap: this.profileCache }
) )
private fetchProfileFromBigRelaysDataloader = new DataLoader<string, TProfile | undefined>( private fetchProfileFromDefaultRelaysDataloader = new DataLoader<string, TProfile | undefined>(
this.profileBatchLoadFn.bind(this), this.profileBatchLoadFn.bind(this),
{ cache: false } { cache: false }
) )
@ -85,6 +86,10 @@ class ClientService {
return this.pool.listConnectionStatus() return this.pool.listConnectionStatus()
} }
setCurrentRelayUrls(urls: string[]) {
this.defaultRelayUrls = Array.from(new Set(urls.concat(BIG_RELAY_URLS)))
}
async publishEvent(relayUrls: string[], event: NEvent) { async publishEvent(relayUrls: string[], event: NEvent) {
return await Promise.any(this.pool.publish(relayUrls, event)) return await Promise.any(this.pool.publish(relayUrls, event))
} }
@ -146,7 +151,6 @@ class ClientService {
oneose() { oneose() {
eosed++ eosed++
if (eosed === started) { if (eosed === started) {
events.sort((a, b) => b.created_at - a.created_at)
onEose(events) onEose(events)
} }
} }
@ -196,7 +200,7 @@ class ClientService {
const events: NEvent[] = [] const events: NEvent[] = []
let hasEosed = false let hasEosed = false
const closer = this.pool.subscribeMany( const closer = this.pool.subscribeMany(
relayUrls.length > 0 ? relayUrls : BIG_RELAY_URLS, relayUrls.length > 0 ? relayUrls : this.defaultRelayUrls,
[ [
{ {
'#e': [parentEventId], '#e': [parentEventId],
@ -280,7 +284,7 @@ class ClientService {
async fetchEvents(relayUrls: string[], filter: Filter, cache = false) { async fetchEvents(relayUrls: string[], filter: Filter, cache = false) {
const events = await this.pool.querySync( const events = await this.pool.querySync(
relayUrls.length > 0 ? relayUrls : BIG_RELAY_URLS, relayUrls.length > 0 ? relayUrls : this.defaultRelayUrls,
filter filter
) )
if (cache) { if (cache) {
@ -374,7 +378,7 @@ class ClientService {
} }
private async fetchEventById(relayUrls: string[], id: string): Promise<NEvent | undefined> { private async fetchEventById(relayUrls: string[], id: string): Promise<NEvent | undefined> {
const event = await this.fetchEventFromBigRelaysDataloader.load(id) const event = await this.fetchEventFromDefaultRelaysDataloader.load(id)
if (event) { if (event) {
return event return event
} }
@ -449,9 +453,9 @@ class ClientService {
throw new Error('Invalid id') throw new Error('Invalid id')
} }
const profileFromBigRelays = await this.fetchProfileFromBigRelaysDataloader.load(pubkey) const profileFromDefaultRelays = await this.fetchProfileFromDefaultRelaysDataloader.load(pubkey)
if (profileFromBigRelays) { if (profileFromDefaultRelays) {
return profileFromBigRelays return profileFromDefaultRelays
} }
const profileEvent = await this.tryHarderToFetchEvent( const profileEvent = await this.tryHarderToFetchEvent(
@ -477,15 +481,15 @@ class ClientService {
private async tryHarderToFetchEvent( private async tryHarderToFetchEvent(
relayUrls: string[], relayUrls: string[],
filter: Filter, filter: Filter,
alreadyFetchedFromBigRelays = false alreadyFetchedFromDefaultRelays = false
) { ) {
if (!relayUrls.length && filter.authors?.length) { if (!relayUrls.length && filter.authors?.length) {
const relayList = await this.fetchRelayList(filter.authors[0]) const relayList = await this.fetchRelayList(filter.authors[0])
relayUrls = alreadyFetchedFromBigRelays relayUrls = alreadyFetchedFromDefaultRelays
? relayList.write.filter((url) => !BIG_RELAY_URLS.includes(url)).slice(0, 4) ? relayList.write.filter((url) => !this.defaultRelayUrls.includes(url)).slice(0, 4)
: relayList.write.slice(0, 4) : relayList.write.slice(0, 4)
} else if (!relayUrls.length && !alreadyFetchedFromBigRelays) { } else if (!relayUrls.length && !alreadyFetchedFromDefaultRelays) {
relayUrls = BIG_RELAY_URLS relayUrls = this.defaultRelayUrls
} }
if (!relayUrls.length) return if (!relayUrls.length) return
@ -494,7 +498,7 @@ class ClientService {
} }
private async eventBatchLoadFn(ids: readonly string[]) { private async eventBatchLoadFn(ids: readonly string[]) {
const events = await this.pool.querySync(BIG_RELAY_URLS, { const events = await this.pool.querySync(this.defaultRelayUrls, {
ids: Array.from(new Set(ids)), ids: Array.from(new Set(ids)),
limit: ids.length limit: ids.length
}) })
@ -507,7 +511,7 @@ class ClientService {
} }
private async profileBatchLoadFn(pubkeys: readonly string[]) { private async profileBatchLoadFn(pubkeys: readonly string[]) {
const events = await this.pool.querySync(BIG_RELAY_URLS, { const events = await this.pool.querySync(this.defaultRelayUrls, {
authors: Array.from(new Set(pubkeys)), authors: Array.from(new Set(pubkeys)),
kinds: [kinds.Metadata], kinds: [kinds.Metadata],
limit: pubkeys.length limit: pubkeys.length
@ -528,7 +532,7 @@ class ClientService {
} }
private async relayListBatchLoadFn(pubkeys: readonly string[]) { private async relayListBatchLoadFn(pubkeys: readonly string[]) {
const events = await this.pool.querySync(BIG_RELAY_URLS, { const events = await this.pool.querySync(this.defaultRelayUrls, {
authors: pubkeys as string[], authors: pubkeys as string[],
kinds: [kinds.RelayList], kinds: [kinds.RelayList],
limit: pubkeys.length limit: pubkeys.length
@ -572,10 +576,13 @@ class ClientService {
private async _fetchFollowListEvent(pubkey: string) { private async _fetchFollowListEvent(pubkey: string) {
const relayList = await this.fetchRelayList(pubkey) const relayList = await this.fetchRelayList(pubkey)
const followListEvents = await this.pool.querySync(relayList.write.concat(BIG_RELAY_URLS), { const followListEvents = await this.pool.querySync(
authors: [pubkey], relayList.write.concat(this.defaultRelayUrls),
kinds: [kinds.Contacts] {
}) authors: [pubkey],
kinds: [kinds.Contacts]
}
)
return followListEvents.sort((a, b) => b.created_at - a.created_at)[0] return followListEvents.sort((a, b) => b.created_at - a.created_at)[0]
} }

7
src/renderer/src/services/storage.service.ts

@ -5,12 +5,7 @@ import { isElectron } from '@renderer/lib/env'
const DEFAULT_RELAY_GROUPS: TRelayGroup[] = [ const DEFAULT_RELAY_GROUPS: TRelayGroup[] = [
{ {
groupName: 'Global', groupName: 'Global',
relayUrls: [ relayUrls: ['wss://relay.damus.io/', 'wss://nos.lol/'],
'wss://relay.damus.io/',
'wss://nos.lol/',
'wss://nostr.mom/',
'wss://relay.primal.net/'
],
isActive: true isActive: true
} }
] ]

1
src/renderer/src/types.ts

@ -15,6 +15,7 @@ export type TRelayList = {
export type TRelayInfo = { export type TRelayInfo = {
supported_nips?: number[] supported_nips?: number[]
software?: string
} }
export type TWebMetadata = { export type TWebMetadata = {

Loading…
Cancel
Save