From 26439f9d6b44c9fa9ed9edba7df622f6a7ed1333 Mon Sep 17 00:00:00 2001 From: codytseng Date: Mon, 18 Nov 2024 17:37:07 +0800 Subject: [PATCH] refactor: improve event and profile retrieval methods --- .../src/components/ImageGallery/index.tsx | 2 +- src/renderer/src/hooks/useFetchEventById.tsx | 45 +---- src/renderer/src/hooks/useFetchProfile.tsx | 31 +--- src/renderer/src/lib/event.ts | 14 +- src/renderer/src/services/client.service.ts | 172 +++++++++++++++--- 5 files changed, 156 insertions(+), 108 deletions(-) diff --git a/src/renderer/src/components/ImageGallery/index.tsx b/src/renderer/src/components/ImageGallery/index.tsx index 9b984b6c..29b3bb6e 100644 --- a/src/renderer/src/components/ImageGallery/index.tsx +++ b/src/renderer/src/components/ImageGallery/index.tsx @@ -32,7 +32,7 @@ export default function ImageGallery({ { diff --git a/src/renderer/src/hooks/useFetchProfile.tsx b/src/renderer/src/hooks/useFetchProfile.tsx index c137746e..e5e462dc 100644 --- a/src/renderer/src/hooks/useFetchProfile.tsx +++ b/src/renderer/src/hooks/useFetchProfile.tsx @@ -1,7 +1,5 @@ -import { formatPubkey } from '@renderer/lib/pubkey' import client from '@renderer/services/client.service' import { TProfile } from '@renderer/types' -import { nip19 } from 'nostr-tools' import { useEffect, useState } from 'react' export function useFetchProfile(id?: string) { @@ -11,7 +9,6 @@ export function useFetchProfile(id?: string) { useEffect(() => { const fetchProfile = async () => { - let pubkey: string | undefined try { if (!id) { setIsFetching(false) @@ -19,39 +16,13 @@ export function useFetchProfile(id?: string) { return } - if (/^[0-9a-f]{64}$/.test(id)) { - pubkey = id - } else { - const { data, type } = nip19.decode(id) - switch (type) { - case 'npub': - pubkey = data - break - case 'nprofile': - pubkey = data.pubkey - break - } - } - - if (!pubkey) { - setIsFetching(false) - setError(new Error('Invalid id')) - return - } - - const profile = await client.fetchProfile(pubkey) + const profile = await client.fetchProfileByBench32Id(id) if (profile) { setProfile(profile) } } catch (err) { setError(err as Error) } finally { - if (pubkey) { - setProfile((pre) => { - if (pre) return pre - return { pubkey, username: formatPubkey(pubkey!) } as TProfile - }) - } setIsFetching(false) } } diff --git a/src/renderer/src/lib/event.ts b/src/renderer/src/lib/event.ts index 26b61c79..f2f0fba5 100644 --- a/src/renderer/src/lib/event.ts +++ b/src/renderer/src/lib/event.ts @@ -55,18 +55,8 @@ export async function extractMentions(content: string, parentEvent?: Event) { pubkeySet.add(data.pubkey) } else if (type === 'npub') { pubkeySet.add(data) - } else if (type === 'nevent') { - eventIdSet.add(data.id) - if (data.author) { - pubkeySet.add(data.author) - } else { - const event = await client.fetchEventById(data.id) - if (event) { - pubkeySet.add(event.pubkey) - } - } - } else if (type === 'note') { - const event = await client.fetchEventById(data) + } else if (['nevent', 'note', 'naddr'].includes(type)) { + const event = await client.fetchEventByBench32Id(id) if (event) { pubkeySet.add(event.pubkey) } diff --git a/src/renderer/src/services/client.service.ts b/src/renderer/src/services/client.service.ts index 67769202..b65e5f1d 100644 --- a/src/renderer/src/services/client.service.ts +++ b/src/renderer/src/services/client.service.ts @@ -10,6 +10,7 @@ import { Filter, kinds, Event as NEvent, + nip19, SimplePool, VerifiedEvent } from 'nostr-tools' @@ -30,24 +31,23 @@ class ClientService { private relayUrls: string[] = BIG_RELAY_URLS private initPromise!: Promise - private eventByFilterCache = new LRUCache>({ - max: 10000, - fetchMethod: async (filterStr) => { - const events = await this.fetchEvents(BIG_RELAY_URLS, JSON.parse(filterStr)) - events.forEach((event) => this.addEventToCache(event)) - return events.sort((a, b) => b.created_at - a.created_at)[0] - } - }) - private eventByIdCache = new LRUCache>({ max: 10000 }) - private eventDataloader = new DataLoader( - this.eventBatchLoadFn.bind(this), - { cacheMap: this.eventByIdCache } + private eventCache = new LRUCache>({ max: 10000 }) + private eventDataLoader = new DataLoader( + (ids) => Promise.all(ids.map((id) => this._fetchEventByBench32Id(id))), + { cacheMap: this.eventCache } ) + private fetchEventFromBigRelaysDataloader = new DataLoader( + this.eventBatchLoadFn.bind(this), + { cache: false } + ) + private profileCache = new LRUCache>({ max: 10000 }) private profileDataloader = new DataLoader( + (ids) => Promise.all(ids.map((id) => this._fetchProfileByBench32Id(id))), + { cacheMap: this.profileCache } + ) + private fetchProfileFromBigRelaysDataloader = new DataLoader( this.profileBatchLoadFn.bind(this), - { - cacheMap: new LRUCache>({ max: 10000 }) - } + { cache: false } ) private relayListDataLoader = new DataLoader( this.relayListBatchLoadFn.bind(this), @@ -127,7 +127,7 @@ class ClientService { } else { events.push(evt) } - that.eventByIdCache.set(evt.id, Promise.resolve(evt)) + that.eventDataLoader.prime(evt.id, Promise.resolve(evt)) }, onclose(reason: string) { if (reason.startsWith('auth-required:')) { @@ -171,20 +171,16 @@ class ClientService { return await this.pool.querySync(relayUrls.length > 0 ? relayUrls : BIG_RELAY_URLS, filter) } - async fetchEventByFilter(filter: Filter) { - return this.eventByFilterCache.fetch(JSON.stringify({ ...filter, limit: 1 })) - } - - async fetchEventById(id: string): Promise { - return this.eventDataloader.load(id) + async fetchEventByBench32Id(id: string): Promise { + return this.eventDataLoader.load(id) } addEventToCache(event: NEvent) { - this.eventByIdCache.set(event.id, Promise.resolve(event)) + this.eventDataLoader.prime(event.id, Promise.resolve(event)) } - async fetchProfile(pubkey: string): Promise { - return this.profileDataloader.load(pubkey) + async fetchProfileByBench32Id(id: string): Promise { + return this.profileDataloader.load(id) } async fetchRelayList(pubkey: string): Promise { @@ -199,9 +195,129 @@ class ClientService { this.followListCache.set(pubkey, Promise.resolve(event)) } + private async fetchEventById(relayUrls: string[], id: string): Promise { + const event = await this.fetchEventFromBigRelaysDataloader.load(id) + if (event) { + return event + } + + return this.tryHarderToFetchEvent(relayUrls, { ids: [id], limit: 1 }, true) + } + + private async _fetchEventByBench32Id(id: string): Promise { + let filter: Filter | undefined + let relays: string[] = [] + if (/^[0-9a-f]{64}$/.test(id)) { + filter = { ids: [id] } + } else { + const { type, data } = nip19.decode(id) + switch (type) { + case 'note': + filter = { ids: [data] } + break + case 'nevent': + filter = { ids: [data.id] } + if (data.relays) relays = data.relays + break + case 'naddr': + filter = { + authors: [data.pubkey], + kinds: [data.kind], + limit: 1 + } + if (data.identifier) { + filter['#d'] = [data.identifier] + } + if (data.relays) relays = data.relays + } + } + if (!filter) { + throw new Error('Invalid id') + } + + let event: NEvent | undefined + if (filter.ids) { + event = await this.fetchEventById(relays, filter.ids[0]) + } else { + event = await this.tryHarderToFetchEvent(relays, filter) + } + + if (event && event.id !== id) { + this.eventDataLoader.prime(event.id, Promise.resolve(event)) + } + + return event + } + + private async _fetchProfileByBench32Id(id: string): Promise { + let pubkey: string | undefined + let relays: string[] = [] + if (/^[0-9a-f]{64}$/.test(id)) { + pubkey = id + } else { + const { data, type } = nip19.decode(id) + switch (type) { + case 'npub': + pubkey = data + break + case 'nprofile': + pubkey = data.pubkey + if (data.relays) relays = data.relays + break + } + } + + if (!pubkey) { + throw new Error('Invalid id') + } + + const profileFromBigRelays = this.fetchProfileFromBigRelaysDataloader.load(pubkey) + if (profileFromBigRelays) { + return profileFromBigRelays + } + + const profileEvent = await this.tryHarderToFetchEvent( + relays, + { + authors: [pubkey], + kinds: [kinds.Metadata], + limit: 1 + }, + true + ) + const profile = profileEvent + ? this.parseProfileFromEvent(profileEvent) + : { pubkey, username: formatPubkey(pubkey) } + + if (profile.pubkey !== id) { + this.profileCache.set(profile.pubkey, Promise.resolve(profile)) + } + + return profile + } + + private async tryHarderToFetchEvent( + relayUrls: string[], + filter: Filter, + alreadyFetchedFromBigRelays = false + ) { + if (!relayUrls.length && filter.authors?.length) { + const relayList = await this.fetchRelayList(filter.authors[0]) + relayUrls = alreadyFetchedFromBigRelays + ? relayList.write.filter((url) => !BIG_RELAY_URLS.includes(url)).slice(0, 4) + : relayList.write.slice(0, 4) + } else if (!relayUrls.length && !alreadyFetchedFromBigRelays) { + relayUrls = BIG_RELAY_URLS + } + if (!relayUrls.length) return + + const events = await this.fetchEvents(relayUrls, filter) + return events.sort((a, b) => b.created_at - a.created_at)[0] + } + private async eventBatchLoadFn(ids: readonly string[]) { const events = await this.fetchEvents(BIG_RELAY_URLS, { - ids: ids as string[], + ids: Array.from(new Set(ids)), limit: ids.length }) const eventsMap = new Map() @@ -214,7 +330,7 @@ class ClientService { private async profileBatchLoadFn(pubkeys: readonly string[]) { const events = await this.fetchEvents(BIG_RELAY_URLS, { - authors: pubkeys as string[], + authors: Array.from(new Set(pubkeys)), kinds: [kinds.Metadata], limit: pubkeys.length }) @@ -229,7 +345,7 @@ class ClientService { return pubkeys.map((pubkey) => { const event = eventsMap.get(pubkey) - return event ? this.parseProfileFromEvent(event) : undefined + return event ? this.parseProfileFromEvent(event) : { pubkey, username: formatPubkey(pubkey) } }) }