diff --git a/src/hooks/useFetchProfile.tsx b/src/hooks/useFetchProfile.tsx index ca3de6df..3e625b75 100644 --- a/src/hooks/useFetchProfile.tsx +++ b/src/hooks/useFetchProfile.tsx @@ -4,7 +4,7 @@ import client from '@/services/client.service' import { TProfile } from '@/types' import { useEffect, useState } from 'react' -export function useFetchProfile(id?: string, skipCache = false) { +export function useFetchProfile(id?: string) { const { profile: currentAccountProfile } = useNostr() const [isFetching, setIsFetching] = useState(true) const [error, setError] = useState(null) @@ -25,7 +25,7 @@ export function useFetchProfile(id?: string, skipCache = false) { const pubkey = userIdToPubkey(id) setPubkey(pubkey) - const profile = await client.fetchProfile(id, skipCache) + const profile = await client.fetchProfile(id) if (profile) { setProfile(profile) } diff --git a/src/lib/pubkey.ts b/src/lib/pubkey.ts index 309bda3e..ea22700f 100644 --- a/src/lib/pubkey.ts +++ b/src/lib/pubkey.ts @@ -38,7 +38,7 @@ export function pubkeyToNpub(pubkey: string) { } } -export function userIdToPubkey(userId: string) { +export function userIdToPubkey(userId: string, throwOnInvalid = false): string { if (userId.startsWith('npub1') || userId.startsWith('nprofile1')) { try { const { type, data } = nip19.decode(userId) @@ -48,6 +48,9 @@ export function userIdToPubkey(userId: string) { return data.pubkey } } catch (error) { + if (throwOnInvalid) { + throw new Error('Invalid id') + } console.error('Error decoding userId:', userId, 'error:', error) } } diff --git a/src/services/client.service.ts b/src/services/client.service.ts index a91d4b28..de8ff023 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -972,7 +972,7 @@ class ClientService extends EventTarget { for (let i = 0; i * 20 < followings.length; i++) { if (signal.aborted) return await Promise.all( - followings.slice(i * 20, (i + 1) * 20).map((pubkey) => this.fetchProfileEvent(pubkey)) + followings.slice(i * 20, (i + 1) * 20).map((pubkey) => this.fetchProfile(pubkey)) ) await new Promise((resolve) => setTimeout(resolve, 1000)) } @@ -1022,7 +1022,7 @@ class ClientService extends EventTarget { } } - async fetchProfileEvent(id: string, skipCache: boolean = false): Promise { + private async _fetchProfileEvent(id: string): Promise { let pubkey: string | undefined let relays: string[] = [] if (/^[0-9a-f]{64}$/.test(id)) { @@ -1043,12 +1043,7 @@ class ClientService extends EventTarget { if (!pubkey) { throw new Error('Invalid id') } - if (!skipCache) { - const localProfile = await indexedDb.getReplaceableEvent(pubkey, kinds.Metadata) - if (localProfile) { - return localProfile - } - } + const profileFromBigRelays = await this.replaceableEventFromBigRelaysDataloader.load({ pubkey, kind: kinds.Metadata @@ -1080,8 +1075,28 @@ class ClientService extends EventTarget { return profileEvent } - async fetchProfile(id: string, skipCache: boolean = false): Promise { - const profileEvent = await this.fetchProfileEvent(id, skipCache) + private profileDataloader = new DataLoader(async (ids) => { + const results = await Promise.allSettled(ids.map((id) => this._fetchProfile(id))) + return results.map((res) => (res.status === 'fulfilled' ? res.value : null)) + }) + + async fetchProfile(id: string, skipCache = false): Promise { + if (skipCache) { + return this._fetchProfile(id) + } + + const pubkey = userIdToPubkey(id, true) + const localProfileEvent = await indexedDb.getReplaceableEvent(pubkey, kinds.Metadata) + if (localProfileEvent) { + this.profileDataloader.load(id) // update cache in background + const localProfile = getProfileFromEvent(localProfileEvent) + return localProfile + } + return await this.profileDataloader.load(id) + } + + private async _fetchProfile(id: string): Promise { + const profileEvent = await this._fetchProfileEvent(id) if (profileEvent) { return getProfileFromEvent(profileEvent) } @@ -1090,7 +1105,7 @@ class ClientService extends EventTarget { const pubkey = userIdToPubkey(id) return { pubkey, npub: pubkeyToNpub(pubkey) ?? '', username: formatPubkey(pubkey) } } catch { - return undefined + return null } } @@ -1100,11 +1115,6 @@ class ClientService extends EventTarget { /** =========== Relay list =========== */ - async fetchRelayListEvent(pubkey: string) { - const [relayEvent] = await this.fetchReplaceableEventsFromBigRelays([pubkey], kinds.RelayList) - return relayEvent ?? null - } - async fetchRelayList(pubkey: string): Promise { const [relayList] = await this.fetchRelayLists([pubkey]) return relayList @@ -1211,12 +1221,16 @@ class ClientService extends EventTarget { } private async updateReplaceableEventFromBigRelaysCache(event: NEvent) { + const newEvent = await indexedDb.putReplaceableEvent(event) + if (newEvent.id !== event.id) { + return + } + this.replaceableEventFromBigRelaysDataloader.clear({ pubkey: event.pubkey, kind: event.kind }) this.replaceableEventFromBigRelaysDataloader.prime( { pubkey: event.pubkey, kind: event.kind }, Promise.resolve(event) ) - await indexedDb.putReplaceableEvent(event) } /** =========== Replaceable event dataloader =========== */ @@ -1301,12 +1315,16 @@ class ClientService extends EventTarget { } private async updateReplaceableEventCache(event: NEvent) { + const newEvent = await indexedDb.putReplaceableEvent(event) + if (newEvent.id !== event.id) { + return + } + this.replaceableEventDataLoader.clear({ pubkey: event.pubkey, kind: event.kind }) this.replaceableEventDataLoader.prime( { pubkey: event.pubkey, kind: event.kind }, Promise.resolve(event) ) - await indexedDb.putReplaceableEvent(event) } /** =========== Replaceable event =========== */ diff --git a/src/services/indexed-db.service.ts b/src/services/indexed-db.service.ts index 405d8a4a..9662ab53 100644 --- a/src/services/indexed-db.service.ts +++ b/src/services/indexed-db.service.ts @@ -485,8 +485,14 @@ class IndexedDbService { } const stores = [ - { name: StoreNames.PROFILE_EVENTS, expirationTimestamp: Date.now() - 1000 * 60 * 60 * 24 }, // 1 day - { name: StoreNames.RELAY_LIST_EVENTS, expirationTimestamp: Date.now() - 1000 * 60 * 60 * 24 }, // 1 day + { + name: StoreNames.PROFILE_EVENTS, + expirationTimestamp: Date.now() - 1000 * 60 * 60 * 24 * 30 // 30 day + }, + { + name: StoreNames.RELAY_LIST_EVENTS, + expirationTimestamp: Date.now() - 1000 * 60 * 60 * 24 // 1 day + }, { name: StoreNames.FOLLOW_LIST_EVENTS, expirationTimestamp: Date.now() - 1000 * 60 * 60 * 24 // 1 day