feat: optimize profile caching mechanism
This commit is contained in:
@@ -4,7 +4,7 @@ import client from '@/services/client.service'
|
|||||||
import { TProfile } from '@/types'
|
import { TProfile } from '@/types'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
export function useFetchProfile(id?: string, skipCache = false) {
|
export function useFetchProfile(id?: string) {
|
||||||
const { profile: currentAccountProfile } = useNostr()
|
const { profile: currentAccountProfile } = useNostr()
|
||||||
const [isFetching, setIsFetching] = useState(true)
|
const [isFetching, setIsFetching] = useState(true)
|
||||||
const [error, setError] = useState<Error | null>(null)
|
const [error, setError] = useState<Error | null>(null)
|
||||||
@@ -25,7 +25,7 @@ export function useFetchProfile(id?: string, skipCache = false) {
|
|||||||
|
|
||||||
const pubkey = userIdToPubkey(id)
|
const pubkey = userIdToPubkey(id)
|
||||||
setPubkey(pubkey)
|
setPubkey(pubkey)
|
||||||
const profile = await client.fetchProfile(id, skipCache)
|
const profile = await client.fetchProfile(id)
|
||||||
if (profile) {
|
if (profile) {
|
||||||
setProfile(profile)
|
setProfile(profile)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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')) {
|
if (userId.startsWith('npub1') || userId.startsWith('nprofile1')) {
|
||||||
try {
|
try {
|
||||||
const { type, data } = nip19.decode(userId)
|
const { type, data } = nip19.decode(userId)
|
||||||
@@ -48,6 +48,9 @@ export function userIdToPubkey(userId: string) {
|
|||||||
return data.pubkey
|
return data.pubkey
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (throwOnInvalid) {
|
||||||
|
throw new Error('Invalid id')
|
||||||
|
}
|
||||||
console.error('Error decoding userId:', userId, 'error:', error)
|
console.error('Error decoding userId:', userId, 'error:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -972,7 +972,7 @@ class ClientService extends EventTarget {
|
|||||||
for (let i = 0; i * 20 < followings.length; i++) {
|
for (let i = 0; i * 20 < followings.length; i++) {
|
||||||
if (signal.aborted) return
|
if (signal.aborted) return
|
||||||
await Promise.all(
|
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))
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
}
|
}
|
||||||
@@ -1022,7 +1022,7 @@ class ClientService extends EventTarget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchProfileEvent(id: string, skipCache: boolean = false): Promise<NEvent | undefined> {
|
private async _fetchProfileEvent(id: string): Promise<NEvent | undefined> {
|
||||||
let pubkey: string | undefined
|
let pubkey: string | undefined
|
||||||
let relays: string[] = []
|
let relays: string[] = []
|
||||||
if (/^[0-9a-f]{64}$/.test(id)) {
|
if (/^[0-9a-f]{64}$/.test(id)) {
|
||||||
@@ -1043,12 +1043,7 @@ class ClientService extends EventTarget {
|
|||||||
if (!pubkey) {
|
if (!pubkey) {
|
||||||
throw new Error('Invalid id')
|
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({
|
const profileFromBigRelays = await this.replaceableEventFromBigRelaysDataloader.load({
|
||||||
pubkey,
|
pubkey,
|
||||||
kind: kinds.Metadata
|
kind: kinds.Metadata
|
||||||
@@ -1080,8 +1075,28 @@ class ClientService extends EventTarget {
|
|||||||
return profileEvent
|
return profileEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchProfile(id: string, skipCache: boolean = false): Promise<TProfile | undefined> {
|
private profileDataloader = new DataLoader<string, TProfile | null, string>(async (ids) => {
|
||||||
const profileEvent = await this.fetchProfileEvent(id, skipCache)
|
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<TProfile | null> {
|
||||||
|
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<TProfile | null> {
|
||||||
|
const profileEvent = await this._fetchProfileEvent(id)
|
||||||
if (profileEvent) {
|
if (profileEvent) {
|
||||||
return getProfileFromEvent(profileEvent)
|
return getProfileFromEvent(profileEvent)
|
||||||
}
|
}
|
||||||
@@ -1090,7 +1105,7 @@ class ClientService extends EventTarget {
|
|||||||
const pubkey = userIdToPubkey(id)
|
const pubkey = userIdToPubkey(id)
|
||||||
return { pubkey, npub: pubkeyToNpub(pubkey) ?? '', username: formatPubkey(pubkey) }
|
return { pubkey, npub: pubkeyToNpub(pubkey) ?? '', username: formatPubkey(pubkey) }
|
||||||
} catch {
|
} catch {
|
||||||
return undefined
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1100,11 +1115,6 @@ class ClientService extends EventTarget {
|
|||||||
|
|
||||||
/** =========== Relay list =========== */
|
/** =========== Relay list =========== */
|
||||||
|
|
||||||
async fetchRelayListEvent(pubkey: string) {
|
|
||||||
const [relayEvent] = await this.fetchReplaceableEventsFromBigRelays([pubkey], kinds.RelayList)
|
|
||||||
return relayEvent ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchRelayList(pubkey: string): Promise<TRelayList> {
|
async fetchRelayList(pubkey: string): Promise<TRelayList> {
|
||||||
const [relayList] = await this.fetchRelayLists([pubkey])
|
const [relayList] = await this.fetchRelayLists([pubkey])
|
||||||
return relayList
|
return relayList
|
||||||
@@ -1211,12 +1221,16 @@ class ClientService extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async updateReplaceableEventFromBigRelaysCache(event: NEvent) {
|
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.clear({ pubkey: event.pubkey, kind: event.kind })
|
||||||
this.replaceableEventFromBigRelaysDataloader.prime(
|
this.replaceableEventFromBigRelaysDataloader.prime(
|
||||||
{ pubkey: event.pubkey, kind: event.kind },
|
{ pubkey: event.pubkey, kind: event.kind },
|
||||||
Promise.resolve(event)
|
Promise.resolve(event)
|
||||||
)
|
)
|
||||||
await indexedDb.putReplaceableEvent(event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** =========== Replaceable event dataloader =========== */
|
/** =========== Replaceable event dataloader =========== */
|
||||||
@@ -1301,12 +1315,16 @@ class ClientService extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async updateReplaceableEventCache(event: NEvent) {
|
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.clear({ pubkey: event.pubkey, kind: event.kind })
|
||||||
this.replaceableEventDataLoader.prime(
|
this.replaceableEventDataLoader.prime(
|
||||||
{ pubkey: event.pubkey, kind: event.kind },
|
{ pubkey: event.pubkey, kind: event.kind },
|
||||||
Promise.resolve(event)
|
Promise.resolve(event)
|
||||||
)
|
)
|
||||||
await indexedDb.putReplaceableEvent(event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** =========== Replaceable event =========== */
|
/** =========== Replaceable event =========== */
|
||||||
|
|||||||
@@ -485,8 +485,14 @@ class IndexedDbService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const stores = [
|
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,
|
name: StoreNames.FOLLOW_LIST_EVENTS,
|
||||||
expirationTimestamp: Date.now() - 1000 * 60 * 60 * 24 // 1 day
|
expirationTimestamp: Date.now() - 1000 * 60 * 60 * 24 // 1 day
|
||||||
|
|||||||
Reference in New Issue
Block a user