perf: cache following favorite relays
This commit is contained in:
@@ -1,9 +1,8 @@
|
|||||||
|
import { useFetchRelayInfo } from '@/hooks'
|
||||||
import { toRelay } from '@/lib/link'
|
import { toRelay } from '@/lib/link'
|
||||||
import { useSecondaryPage } from '@/PageManager'
|
import { useSecondaryPage } from '@/PageManager'
|
||||||
import { useNostr } from '@/providers/NostrProvider'
|
import { useNostr } from '@/providers/NostrProvider'
|
||||||
import client from '@/services/client.service'
|
import client from '@/services/client.service'
|
||||||
import relayInfoService from '@/services/relay-info.service'
|
|
||||||
import { TNip66RelayInfo } from '@/types'
|
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import RelaySimpleInfo, { RelaySimpleInfoSkeleton } from '../RelaySimpleInfo'
|
import RelaySimpleInfo, { RelaySimpleInfoSkeleton } from '../RelaySimpleInfo'
|
||||||
@@ -12,10 +11,9 @@ const SHOW_COUNT = 10
|
|||||||
|
|
||||||
export default function FollowingFavoriteRelayList() {
|
export default function FollowingFavoriteRelayList() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { push } = useSecondaryPage()
|
|
||||||
const { pubkey } = useNostr()
|
const { pubkey } = useNostr()
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [relays, setRelays] = useState<(TNip66RelayInfo & { users: string[] })[]>([])
|
const [relays, setRelays] = useState<[string, string[]][]>([])
|
||||||
const [showCount, setShowCount] = useState(SHOW_COUNT)
|
const [showCount, setShowCount] = useState(SHOW_COUNT)
|
||||||
const bottomRef = useRef<HTMLDivElement>(null)
|
const bottomRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
@@ -25,21 +23,8 @@ export default function FollowingFavoriteRelayList() {
|
|||||||
const init = async () => {
|
const init = async () => {
|
||||||
if (!pubkey) return
|
if (!pubkey) return
|
||||||
|
|
||||||
const relayMap =
|
const relays = (await client.fetchFollowingFavoriteRelays(pubkey)) ?? []
|
||||||
(await client.fetchFollowingFavoriteRelays(pubkey)) ?? new Map<string, Set<string>>()
|
setRelays(relays)
|
||||||
const relayUrls = Array.from(relayMap.keys())
|
|
||||||
const relayInfos = await relayInfoService.getRelayInfos(relayUrls ?? [])
|
|
||||||
setRelays(
|
|
||||||
(relayInfos.filter(Boolean) as TNip66RelayInfo[])
|
|
||||||
.map((relayInfo) => {
|
|
||||||
const users = Array.from(relayMap.get(relayInfo.url) ?? [])
|
|
||||||
return {
|
|
||||||
...relayInfo,
|
|
||||||
users
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.sort((a, b) => b.users.length - a.users.length)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
init().finally(() => {
|
init().finally(() => {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
@@ -73,16 +58,8 @@ export default function FollowingFavoriteRelayList() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{relays.slice(0, showCount).map((relay) => (
|
{relays.slice(0, showCount).map(([url, users]) => (
|
||||||
<RelaySimpleInfo
|
<RelayItem key={url} url={url} users={users} />
|
||||||
key={relay.url}
|
|
||||||
relayInfo={relay}
|
|
||||||
className="clickable p-4 border-b"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
push(toRelay(relay.url))
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
{showCount < relays.length && <div ref={bottomRef} />}
|
{showCount < relays.length && <div ref={bottomRef} />}
|
||||||
{loading && <RelaySimpleInfoSkeleton />}
|
{loading && <RelaySimpleInfoSkeleton />}
|
||||||
@@ -94,3 +71,21 @@ export default function FollowingFavoriteRelayList() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RelayItem({ url, users }: { url: string; users: string[] }) {
|
||||||
|
const { push } = useSecondaryPage()
|
||||||
|
const { relayInfo } = useFetchRelayInfo(url)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RelaySimpleInfo
|
||||||
|
key={url}
|
||||||
|
relayInfo={relayInfo}
|
||||||
|
users={users}
|
||||||
|
className="clickable p-4 border-b"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
push(toRelay(url))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ import { SimpleUserAvatar } from '../UserAvatar'
|
|||||||
|
|
||||||
export default function RelaySimpleInfo({
|
export default function RelaySimpleInfo({
|
||||||
relayInfo,
|
relayInfo,
|
||||||
|
users,
|
||||||
hideBadge = false,
|
hideBadge = false,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: HTMLProps<HTMLDivElement> & {
|
}: HTMLProps<HTMLDivElement> & {
|
||||||
relayInfo?: TNip66RelayInfo & { users?: string[] }
|
relayInfo?: TNip66RelayInfo
|
||||||
|
users?: string[]
|
||||||
hideBadge?: boolean
|
hideBadge?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -35,16 +37,16 @@ export default function RelaySimpleInfo({
|
|||||||
</div>
|
</div>
|
||||||
{!hideBadge && relayInfo && <RelayBadges relayInfo={relayInfo} />}
|
{!hideBadge && relayInfo && <RelayBadges relayInfo={relayInfo} />}
|
||||||
{!!relayInfo?.description && <div className="line-clamp-4">{relayInfo.description}</div>}
|
{!!relayInfo?.description && <div className="line-clamp-4">{relayInfo.description}</div>}
|
||||||
{!!relayInfo?.users?.length && (
|
{!!users?.length && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="text-muted-foreground">{t('Favorited by')} </div>
|
<div className="text-muted-foreground">{t('Favorited by')} </div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{relayInfo.users.slice(0, 10).map((user) => (
|
{users.slice(0, 10).map((user) => (
|
||||||
<SimpleUserAvatar key={user} userId={user} size="xSmall" />
|
<SimpleUserAvatar key={user} userId={user} size="xSmall" />
|
||||||
))}
|
))}
|
||||||
{relayInfo.users.length > 10 && (
|
{users.length > 10 && (
|
||||||
<div className="text-muted-foreground text-xs rounded-full bg-muted w-5 h-5 flex items-center justify-center">
|
<div className="text-muted-foreground text-xs rounded-full bg-muted w-5 h-5 flex items-center justify-center">
|
||||||
+{relayInfo.users.length - 10}
|
+{users.length - 10}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -74,10 +74,7 @@ class ClientService extends EventTarget {
|
|||||||
max: 2000,
|
max: 2000,
|
||||||
fetchMethod: this._fetchFollowListEvent.bind(this)
|
fetchMethod: this._fetchFollowListEvent.bind(this)
|
||||||
})
|
})
|
||||||
private fetchFollowingFavoriteRelaysCache = new LRUCache<
|
private fetchFollowingFavoriteRelaysCache = new LRUCache<string, Promise<[string, string[]][]>>({
|
||||||
string,
|
|
||||||
Promise<Map<string, Set<string>>>
|
|
||||||
>({
|
|
||||||
max: 10,
|
max: 10,
|
||||||
fetchMethod: this._fetchFollowingFavoriteRelays.bind(this)
|
fetchMethod: this._fetchFollowingFavoriteRelays.bind(this)
|
||||||
})
|
})
|
||||||
@@ -823,40 +820,54 @@ class ClientService extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _fetchFollowingFavoriteRelays(pubkey: string) {
|
private async _fetchFollowingFavoriteRelays(pubkey: string) {
|
||||||
const followings = await this.fetchFollowings(pubkey)
|
const fetchNewData = async () => {
|
||||||
const events = await this.fetchEvents(BIG_RELAY_URLS, {
|
const followings = await this.fetchFollowings(pubkey)
|
||||||
authors: followings,
|
const events = await this.fetchEvents(BIG_RELAY_URLS, {
|
||||||
kinds: [ExtendedKind.FAVORITE_RELAYS, kinds.Relaysets],
|
authors: followings,
|
||||||
limit: 1000
|
kinds: [ExtendedKind.FAVORITE_RELAYS, kinds.Relaysets],
|
||||||
})
|
limit: 1000
|
||||||
const alreadyExistsFavoriteRelaysPubkeySet = new Set<string>()
|
|
||||||
const alreadyExistsRelaySetsPubkeySet = new Set<string>()
|
|
||||||
const uniqueEvents: NEvent[] = []
|
|
||||||
events
|
|
||||||
.sort((a, b) => b.created_at - a.created_at)
|
|
||||||
.forEach((event) => {
|
|
||||||
if (event.kind === ExtendedKind.FAVORITE_RELAYS) {
|
|
||||||
if (alreadyExistsFavoriteRelaysPubkeySet.has(event.pubkey)) return
|
|
||||||
alreadyExistsFavoriteRelaysPubkeySet.add(event.pubkey)
|
|
||||||
} else if (event.kind === kinds.Relaysets) {
|
|
||||||
if (alreadyExistsRelaySetsPubkeySet.has(event.pubkey)) return
|
|
||||||
alreadyExistsRelaySetsPubkeySet.add(event.pubkey)
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
uniqueEvents.push(event)
|
|
||||||
})
|
})
|
||||||
|
const alreadyExistsFavoriteRelaysPubkeySet = new Set<string>()
|
||||||
|
const alreadyExistsRelaySetsPubkeySet = new Set<string>()
|
||||||
|
const uniqueEvents: NEvent[] = []
|
||||||
|
events
|
||||||
|
.sort((a, b) => b.created_at - a.created_at)
|
||||||
|
.forEach((event) => {
|
||||||
|
if (event.kind === ExtendedKind.FAVORITE_RELAYS) {
|
||||||
|
if (alreadyExistsFavoriteRelaysPubkeySet.has(event.pubkey)) return
|
||||||
|
alreadyExistsFavoriteRelaysPubkeySet.add(event.pubkey)
|
||||||
|
} else if (event.kind === kinds.Relaysets) {
|
||||||
|
if (alreadyExistsRelaySetsPubkeySet.has(event.pubkey)) return
|
||||||
|
alreadyExistsRelaySetsPubkeySet.add(event.pubkey)
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uniqueEvents.push(event)
|
||||||
|
})
|
||||||
|
|
||||||
const relayMap = new Map<string, Set<string>>()
|
const relayMap = new Map<string, Set<string>>()
|
||||||
uniqueEvents.forEach((event) => {
|
uniqueEvents.forEach((event) => {
|
||||||
event.tags.forEach(([tagName, tagValue]) => {
|
event.tags.forEach(([tagName, tagValue]) => {
|
||||||
if (tagName === 'relay' && tagValue && isWebsocketUrl(tagValue)) {
|
if (tagName === 'relay' && tagValue && isWebsocketUrl(tagValue)) {
|
||||||
const url = normalizeUrl(tagValue)
|
const url = normalizeUrl(tagValue)
|
||||||
relayMap.set(url, (relayMap.get(url) || new Set()).add(event.pubkey))
|
relayMap.set(url, (relayMap.get(url) || new Set()).add(event.pubkey))
|
||||||
}
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
const relayMapEntries = Array.from(relayMap.entries())
|
||||||
return relayMap
|
.sort((a, b) => b[1].size - a[1].size)
|
||||||
|
.map(([url, pubkeys]) => [url, Array.from(pubkeys)]) as [string, string[]][]
|
||||||
|
|
||||||
|
indexedDb.putFollowingFavoriteRelays(pubkey, relayMapEntries)
|
||||||
|
return relayMapEntries
|
||||||
|
}
|
||||||
|
|
||||||
|
const cached = await indexedDb.getFollowingFavoriteRelays(pubkey)
|
||||||
|
if (cached) {
|
||||||
|
fetchNewData()
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
return fetchNewData()
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFollowListCache(event: NEvent) {
|
updateFollowListCache(event: NEvent) {
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ const StoreNames = {
|
|||||||
MUTE_DECRYPTED_TAGS: 'muteDecryptedTags',
|
MUTE_DECRYPTED_TAGS: 'muteDecryptedTags',
|
||||||
RELAY_INFO_EVENTS: 'relayInfoEvents',
|
RELAY_INFO_EVENTS: 'relayInfoEvents',
|
||||||
FAVORITE_RELAYS: 'favoriteRelays',
|
FAVORITE_RELAYS: 'favoriteRelays',
|
||||||
RELAY_SETS: 'relaySets'
|
RELAY_SETS: 'relaySets',
|
||||||
|
FOLLOWING_FAVORITE_RELAYS: 'followingFavoriteRelays'
|
||||||
}
|
}
|
||||||
|
|
||||||
class IndexedDbService {
|
class IndexedDbService {
|
||||||
@@ -36,7 +37,7 @@ class IndexedDbService {
|
|||||||
init(): Promise<void> {
|
init(): Promise<void> {
|
||||||
if (!this.initPromise) {
|
if (!this.initPromise) {
|
||||||
this.initPromise = new Promise((resolve, reject) => {
|
this.initPromise = new Promise((resolve, reject) => {
|
||||||
const request = window.indexedDB.open('jumble', 4)
|
const request = window.indexedDB.open('jumble', 5)
|
||||||
|
|
||||||
request.onerror = (event) => {
|
request.onerror = (event) => {
|
||||||
reject(event)
|
reject(event)
|
||||||
@@ -76,6 +77,9 @@ class IndexedDbService {
|
|||||||
if (!db.objectStoreNames.contains(StoreNames.RELAY_SETS)) {
|
if (!db.objectStoreNames.contains(StoreNames.RELAY_SETS)) {
|
||||||
db.createObjectStore(StoreNames.RELAY_SETS, { keyPath: 'key' })
|
db.createObjectStore(StoreNames.RELAY_SETS, { keyPath: 'key' })
|
||||||
}
|
}
|
||||||
|
if (!db.objectStoreNames.contains(StoreNames.FOLLOWING_FAVORITE_RELAYS)) {
|
||||||
|
db.createObjectStore(StoreNames.FOLLOWING_FAVORITE_RELAYS, { keyPath: 'key' })
|
||||||
|
}
|
||||||
this.db = db
|
this.db = db
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -363,6 +367,50 @@ class IndexedDbService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async putFollowingFavoriteRelays(pubkey: string, relays: [string, string[]][]): Promise<void> {
|
||||||
|
await this.initPromise
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this.db) {
|
||||||
|
return reject('database not initialized')
|
||||||
|
}
|
||||||
|
const transaction = this.db.transaction(StoreNames.FOLLOWING_FAVORITE_RELAYS, 'readwrite')
|
||||||
|
const store = transaction.objectStore(StoreNames.FOLLOWING_FAVORITE_RELAYS)
|
||||||
|
|
||||||
|
const putRequest = store.put(this.formatValue(pubkey, relays))
|
||||||
|
putRequest.onsuccess = () => {
|
||||||
|
transaction.commit()
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
putRequest.onerror = (event) => {
|
||||||
|
transaction.commit()
|
||||||
|
reject(event)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFollowingFavoriteRelays(pubkey: string): Promise<[string, string[]][] | null> {
|
||||||
|
await this.initPromise
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this.db) {
|
||||||
|
return reject('database not initialized')
|
||||||
|
}
|
||||||
|
const transaction = this.db.transaction(StoreNames.FOLLOWING_FAVORITE_RELAYS, 'readonly')
|
||||||
|
const store = transaction.objectStore(StoreNames.FOLLOWING_FAVORITE_RELAYS)
|
||||||
|
const request = store.get(pubkey)
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
transaction.commit()
|
||||||
|
resolve((request.result as TValue<[string, string[]][]>)?.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.onerror = (event) => {
|
||||||
|
transaction.commit()
|
||||||
|
reject(event)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private getReplaceableEventKey(event: Event): string {
|
private getReplaceableEventKey(event: Event): string {
|
||||||
if (
|
if (
|
||||||
[kinds.Metadata, kinds.Contacts].includes(event.kind) ||
|
[kinds.Metadata, kinds.Contacts].includes(event.kind) ||
|
||||||
|
|||||||
Reference in New Issue
Block a user