Browse Source

refactor: client service

imwald
codytseng 7 months ago
parent
commit
21f09426cf
  1. 2
      src/components/PostEditor/PostTextarea/suggestion.ts
  2. 2
      src/hooks/useSearchProfiles.tsx
  3. 2
      src/lib/event-metadata.ts
  4. 2
      src/lib/event.ts
  5. 2
      src/providers/NostrProvider/index.tsx
  6. 14
      src/providers/UserTrustProvider.tsx
  7. 788
      src/services/client.service.ts
  8. 2
      src/services/indexed-db.service.ts

2
src/components/PostEditor/PostTextarea/suggestion.ts

@ -8,7 +8,7 @@ import MentionList, { MentionListHandle, MentionListProps } from './MentionList'
const suggestion = { const suggestion = {
items: async ({ query }: { query: string }) => { items: async ({ query }: { query: string }) => {
return await client.searchNpubsFromCache(query, 20) return await client.searchNpubsFromLocal(query, 20)
}, },
render: () => { render: () => {

2
src/hooks/useSearchProfiles.tsx

@ -22,7 +22,7 @@ export function useSearchProfiles(search: string, limit: number) {
setIsFetching(true) setIsFetching(true)
setProfiles([]) setProfiles([])
try { try {
const profiles = await client.searchProfilesFromCache(search, limit) const profiles = await client.searchProfilesFromLocal(search, limit)
setProfiles(profiles) setProfiles(profiles)
if (profiles.length >= limit) { if (profiles.length >= limit) {
return return

2
src/lib/event-metadata.ts

@ -8,7 +8,7 @@ import { generateBech32IdFromETag, tagNameEquals } from './tag'
import { isWebsocketUrl, normalizeHttpUrl, normalizeUrl } from './url' import { isWebsocketUrl, normalizeHttpUrl, normalizeUrl } from './url'
import { isTorBrowser } from './utils' import { isTorBrowser } from './utils'
export function getRelayListFromEvent(event?: Event) { export function getRelayListFromEvent(event?: Event | null) {
if (!event) { if (!event) {
return { write: BIG_RELAY_URLS, read: BIG_RELAY_URLS, originalRelays: [] } return { write: BIG_RELAY_URLS, read: BIG_RELAY_URLS, originalRelays: [] }
} }

2
src/lib/event.ts

@ -222,7 +222,7 @@ export function getEmbeddedPubkeys(event: Event) {
return embeddedPubkeys return embeddedPubkeys
} }
export function getLatestEvent(events: Event[]) { export function getLatestEvent(events: Event[]): Event | undefined {
return events.sort((a, b) => b.created_at - a.created_at)[0] return events.sort((a, b) => b.created_at - a.created_at)[0]
} }

2
src/providers/NostrProvider/index.tsx

@ -650,7 +650,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
if (newFollowListEvent.id !== followListEvent.id) return if (newFollowListEvent.id !== followListEvent.id) return
setFollowListEvent(newFollowListEvent) setFollowListEvent(newFollowListEvent)
client.updateFollowListCache(newFollowListEvent) await client.updateFollowListCache(newFollowListEvent)
} }
const updateMuteListEvent = async (muteListEvent: Event, privateTags: string[][]) => { const updateMuteListEvent = async (muteListEvent: Event, privateTags: string[][]) => {

14
src/providers/UserTrustProvider.tsx

@ -42,13 +42,21 @@ export function UserTrustProvider({ children }: { children: React.ReactNode }) {
const initWoT = async () => { const initWoT = async () => {
const followings = await client.fetchFollowings(currentPubkey) const followings = await client.fetchFollowings(currentPubkey)
followings.forEach((pubkey) => wotSet.add(pubkey))
const batchSize = 20
for (let i = 0; i < followings.length; i += batchSize) {
const batch = followings.slice(i, i + batchSize)
await Promise.allSettled( await Promise.allSettled(
followings.map(async (pubkey) => { batch.map(async (pubkey) => {
wotSet.add(pubkey)
const _followings = await client.fetchFollowings(pubkey) const _followings = await client.fetchFollowings(pubkey)
_followings.forEach((following) => wotSet.add(following)) _followings.forEach((following) => {
wotSet.add(following)
})
}) })
) )
await new Promise((resolve) => setTimeout(resolve, 200))
}
} }
initWoT() initWoT()
}, [currentPubkey]) }, [currentPubkey])

788
src/services/client.service.ts

@ -1,4 +1,5 @@
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants' import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
import { getLatestEvent } from '@/lib/event'
import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata' import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata'
import { formatPubkey, pubkeyToNpub, userIdToPubkey } from '@/lib/pubkey' import { formatPubkey, pubkeyToNpub, userIdToPubkey } from '@/lib/pubkey'
import { getPubkeysFromPTags, getServersFromServerTags } from '@/lib/tag' import { getPubkeysFromPTags, getServersFromServerTags } from '@/lib/tag'
@ -50,39 +51,6 @@ class ClientService extends EventTarget {
this.fetchEventsFromBigRelays.bind(this), this.fetchEventsFromBigRelays.bind(this),
{ cache: false, batchScheduleFn: (callback) => setTimeout(callback, 50) } { cache: false, batchScheduleFn: (callback) => setTimeout(callback, 50) }
) )
private fetchProfileEventFromBigRelaysDataloader = new DataLoader<string, NEvent | undefined>(
this.profileEventBatchLoadFn.bind(this),
{
batchScheduleFn: (callback) => setTimeout(callback, 50),
maxBatchSize: 500
}
)
private fetchFollowListEventFromBigRelaysDataloader = new DataLoader<string, NEvent | undefined>(
this.followListEventBatchLoadFn.bind(this),
{
batchScheduleFn: (callback) => setTimeout(callback, 50),
maxBatchSize: 500
}
)
private relayListEventDataLoader = new DataLoader<string, NEvent | undefined>(
this.relayListEventBatchLoadFn.bind(this),
{
batchScheduleFn: (callback) => setTimeout(callback, 50),
maxBatchSize: 500
}
)
private followListCache = new LRUCache<string, Promise<NEvent | undefined>>({
max: 2000,
fetchMethod: this._fetchFollowListEvent.bind(this)
})
private followingFavoriteRelaysCache = new LRUCache<string, Promise<[string, string[]][]>>({
max: 10,
fetchMethod: this._fetchFollowingFavoriteRelays.bind(this)
})
private blossomServerListEventCache = new LRUCache<string, Promise<NEvent | null>>({
max: 1000,
fetchMethod: this._fetchBlossomServerListEvent.bind(this)
})
private userIndex = new FlexSearch.Index({ private userIndex = new FlexSearch.Index({
tokenize: 'forward' tokenize: 'forward'
@ -169,6 +137,8 @@ class ClientService extends EventTarget {
return 'Nostr ' + btoa(JSON.stringify(event)) return 'Nostr ' + btoa(JSON.stringify(event))
} }
/** =========== Timeline =========== */
private generateTimelineKey(urls: string[], filter: Filter) { private generateTimelineKey(urls: string[], filter: Filter) {
const stableFilter: any = {} const stableFilter: any = {}
Object.entries(filter) Object.entries(filter)
@ -427,27 +397,6 @@ class ClientService extends EventTarget {
} }
} }
private async query(urls: string[], filter: Filter | Filter[], onevent?: (evt: NEvent) => void) {
return await new Promise<NEvent[]>((resolve) => {
const events: NEvent[] = []
const sub = this.subscribe(urls, filter, {
onevent(evt) {
onevent?.(evt)
events.push(evt)
},
oneose: (eosed) => {
if (eosed) {
sub.close()
resolve(events)
}
},
onclose: () => {
resolve(events)
}
})
})
}
private async _subscribeTimeline( private async _subscribeTimeline(
urls: string[], urls: string[],
filter: Omit<Filter, 'since' | 'until'> & { limit: number }, // filter with limit, filter: Omit<Filter, 'since' | 'until'> & { limit: number }, // filter with limit,
@ -610,6 +559,54 @@ class ClientService extends EventTarget {
return [...cachedEvents, ...events] return [...cachedEvents, ...events]
} }
/** =========== Event =========== */
getSeenEventRelays(eventId: string) {
return Array.from(this.pool.seenOn.get(eventId)?.values() || [])
}
getSeenEventRelayUrls(eventId: string) {
return this.getSeenEventRelays(eventId).map((relay) => relay.url)
}
getEventHints(eventId: string) {
return this.getSeenEventRelayUrls(eventId).filter((url) => !isLocalNetworkUrl(url))
}
getEventHint(eventId: string) {
return this.getSeenEventRelayUrls(eventId).find((url) => !isLocalNetworkUrl(url)) ?? ''
}
trackEventSeenOn(eventId: string, relay: AbstractRelay) {
let set = this.pool.seenOn.get(eventId)
if (!set) {
set = new Set()
this.pool.seenOn.set(eventId, set)
}
set.add(relay)
}
private async query(urls: string[], filter: Filter | Filter[], onevent?: (evt: NEvent) => void) {
return await new Promise<NEvent[]>((resolve) => {
const events: NEvent[] = []
const sub = this.subscribe(urls, filter, {
onevent(evt) {
onevent?.(evt)
events.push(evt)
},
oneose: (eosed) => {
if (eosed) {
sub.close()
resolve(events)
}
},
onclose: () => {
resolve(events)
}
})
})
}
async fetchEvents( async fetchEvents(
urls: string[], urls: string[],
filter: Filter | Filter[], filter: Filter | Filter[],
@ -661,175 +658,108 @@ class ClientService extends EventTarget {
this.eventDataLoader.prime(event.id, Promise.resolve(event)) this.eventDataLoader.prime(event.id, Promise.resolve(event))
} }
async fetchProfileEvent(id: string, skipCache: boolean = false): Promise<NEvent | undefined> { private async fetchEventById(relayUrls: string[], id: string): Promise<NEvent | undefined> {
let pubkey: string | undefined const event = await this.fetchEventFromBigRelaysDataloader.load(id)
if (event) {
return event
}
return this.tryHarderToFetchEvent(relayUrls, { ids: [id], limit: 1 }, true)
}
private async _fetchEvent(id: string): Promise<NEvent | undefined> {
let filter: Filter | undefined
let relays: string[] = [] let relays: string[] = []
let author: string | undefined
if (/^[0-9a-f]{64}$/.test(id)) { if (/^[0-9a-f]{64}$/.test(id)) {
pubkey = id filter = { ids: [id] }
} else { } else {
const { data, type } = nip19.decode(id) const { type, data } = nip19.decode(id)
switch (type) { switch (type) {
case 'npub': case 'note':
pubkey = data filter = { ids: [data] }
break break
case 'nprofile': case 'nevent':
pubkey = data.pubkey filter = { ids: [data.id] }
if (data.relays) relays = data.relays if (data.relays) relays = data.relays
if (data.author) author = data.author
break break
case 'naddr':
filter = {
authors: [data.pubkey],
kinds: [data.kind],
limit: 1
} }
author = data.pubkey
if (data.identifier) {
filter['#d'] = [data.identifier]
} }
if (data.relays) relays = data.relays
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.fetchProfileEventFromBigRelaysDataloader.load(pubkey)
if (profileFromBigRelays) {
this.addUsernameToIndex(profileFromBigRelays)
return profileFromBigRelays
}
if (!relays.length) {
return undefined
}
const profileEvent = await this.tryHarderToFetchEvent(
relays,
{
authors: [pubkey],
kinds: [kinds.Metadata],
limit: 1
},
true
)
if (profileEvent) {
this.addUsernameToIndex(profileEvent)
indexedDb.putReplaceableEvent(profileEvent)
} }
return profileEvent
} }
if (!filter) {
async fetchProfile(id: string, skipCache: boolean = false): Promise<TProfile | undefined> { throw new Error('Invalid id')
const profileEvent = await this.fetchProfileEvent(id, skipCache)
if (profileEvent) {
return getProfileFromEvent(profileEvent)
} }
try { let event: NEvent | undefined
const pubkey = userIdToPubkey(id) if (filter.ids) {
return { pubkey, npub: pubkeyToNpub(pubkey) ?? '', username: formatPubkey(pubkey) } event = await this.fetchEventById(relays, filter.ids[0])
} catch { } else {
return undefined if (author) {
const relayList = await this.fetchRelayList(author)
relays.push(...relayList.write.slice(0, 4))
} }
event = await this.tryHarderToFetchEvent(relays, filter)
} }
async searchProfiles(relayUrls: string[], filter: Filter): Promise<TProfile[]> { if (event && event.id !== id) {
const events = await this.query(relayUrls, { this.eventDataLoader.prime(event.id, Promise.resolve(event))
...filter,
kinds: [kinds.Metadata]
})
const profileEvents = events.sort((a, b) => b.created_at - a.created_at)
await Promise.allSettled(profileEvents.map((profile) => this.addUsernameToIndex(profile)))
profileEvents.forEach((profile) => this.updateProfileEventCache(profile))
return profileEvents.map((profileEvent) => getProfileFromEvent(profileEvent))
} }
async fetchRelayListEvent(pubkey: string) { return event
return this.relayListEventDataLoader.load(pubkey)
} }
async fetchRelayList(pubkey: string): Promise<TRelayList> { private async tryHarderToFetchEvent(
const [relayList] = await this.fetchRelayLists([pubkey]) relayUrls: string[],
return relayList 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
async fetchRelayLists(pubkeys: string[]): Promise<TRelayList[]> { const events = await this.query(relayUrls, filter)
const relayEvents = await indexedDb.getManyReplaceableEvents(pubkeys, kinds.RelayList) return events.sort((a, b) => b.created_at - a.created_at)[0]
const nonExistingPubkeyIndexMap = new Map<string, number>()
pubkeys.forEach((pubkey, i) => {
if (relayEvents[i] === undefined) {
nonExistingPubkeyIndexMap.set(pubkey, i)
}
})
const newEvents = await this.relayListEventDataLoader.loadMany(
Array.from(nonExistingPubkeyIndexMap.keys())
)
newEvents.forEach((event) => {
if (event && !(event instanceof Error)) {
const index = nonExistingPubkeyIndexMap.get(event.pubkey)
if (index !== undefined) {
relayEvents[index] = event
}
} }
})
return relayEvents.map((event) => { private async fetchEventsFromBigRelays(ids: readonly string[]) {
if (event) { const events = await this.query(BIG_RELAY_URLS, {
return getRelayListFromEvent(event) ids: Array.from(new Set(ids)),
} limit: ids.length
return {
write: BIG_RELAY_URLS,
read: BIG_RELAY_URLS,
originalRelays: []
}
}) })
const eventsMap = new Map<string, NEvent>()
for (const event of events) {
eventsMap.set(event.id, event)
} }
async forceUpdateRelayListEvent(pubkey: string) { return ids.map((id) => eventsMap.get(id))
await this.relayListEventBatchLoadFn([pubkey])
} }
async fetchFollowListEvent(pubkey: string) { /** =========== Following favorite relays =========== */
return await this.followListCache.fetch(pubkey)
}
async fetchMuteListEvent(pubkey: string): Promise<NEvent | undefined> { private followingFavoriteRelaysCache = new LRUCache<string, Promise<[string, string[]][]>>({
const storedEvent = await indexedDb.getReplaceableEvent(pubkey, kinds.Mutelist) max: 10,
if (storedEvent) { fetchMethod: this._fetchFollowingFavoriteRelays.bind(this)
return storedEvent
}
const relayList = await this.fetchRelayList(pubkey)
const events = await this.query(relayList.write.concat(BIG_RELAY_URLS), {
authors: [pubkey],
kinds: [kinds.Mutelist]
}) })
const muteList = events.sort((a, b) => b.created_at - a.created_at)[0]
if (muteList) {
await indexedDb.putReplaceableEvent(muteList)
}
return muteList
}
async fetchBookmarkListEvent(pubkey: string): Promise<NEvent | undefined> { async fetchFollowingFavoriteRelays(pubkey: string) {
const storedBookmarkListEvent = await indexedDb.getReplaceableEvent(pubkey, kinds.BookmarkList) return this.followingFavoriteRelaysCache.fetch(pubkey)
if (storedBookmarkListEvent) {
return storedBookmarkListEvent
}
const relayList = await this.fetchRelayList(pubkey)
const events = await this.query(relayList.write.concat(BIG_RELAY_URLS), {
authors: [pubkey],
kinds: [kinds.BookmarkList]
})
return events.sort((a, b) => b.created_at - a.created_at)[0]
}
async fetchFollowings(pubkey: string) {
const followListEvent = await this.fetchFollowListEvent(pubkey)
return followListEvent ? getPubkeysFromPTags(followListEvent.tags) : []
}
async fetchFollowingFavoriteRelays(pubkey: string) {
return this.followingFavoriteRelaysCache.fetch(pubkey)
} }
private async _fetchFollowingFavoriteRelays(pubkey: string) { private async _fetchFollowingFavoriteRelays(pubkey: string) {
@ -883,73 +813,7 @@ class ClientService extends EventTarget {
return fetchNewData() return fetchNewData()
} }
async fetchBlossomServerList(pubkey: string) { /** =========== Followings =========== */
const evt = await this.blossomServerListEventCache.fetch(pubkey)
return evt ? getServersFromServerTags(evt.tags) : []
}
async fetchBlossomServerListEvent(pubkey: string) {
return (await this.blossomServerListEventCache.fetch(pubkey)) ?? null
}
async updateBlossomServerListEventCache(evt: NEvent) {
this.blossomServerListEventCache.set(evt.pubkey, Promise.resolve(evt))
await indexedDb.putReplaceableEvent(evt)
}
private async _fetchBlossomServerListEvent(pubkey: string) {
const fetchNew = async () => {
const relayList = await this.fetchRelayList(pubkey)
const events = await this.fetchEvents(relayList.write.concat(BIG_RELAY_URLS).slice(0, 5), {
authors: [pubkey],
kinds: [ExtendedKind.BLOSSOM_SERVER_LIST]
})
const blossomServerListEvent = events.sort((a, b) => b.created_at - a.created_at)[0]
if (!blossomServerListEvent) {
indexedDb.putNullReplaceableEvent(pubkey, ExtendedKind.BLOSSOM_SERVER_LIST)
return null
}
indexedDb.putReplaceableEvent(blossomServerListEvent)
return blossomServerListEvent
}
const storedBlossomServerListEvent = await indexedDb.getReplaceableEvent(
pubkey,
ExtendedKind.BLOSSOM_SERVER_LIST
)
if (storedBlossomServerListEvent) {
fetchNew()
return storedBlossomServerListEvent
}
return fetchNew()
}
updateFollowListCache(event: NEvent) {
this.followListCache.set(event.pubkey, Promise.resolve(event))
indexedDb.putReplaceableEvent(event)
}
updateRelayListCache(event: NEvent) {
this.relayListEventDataLoader.clear(event.pubkey)
this.relayListEventDataLoader.prime(event.pubkey, Promise.resolve(event))
indexedDb.putReplaceableEvent(event)
}
updateProfileEventCache(event: NEvent) {
this.fetchProfileEventFromBigRelaysDataloader.clear(event.pubkey)
this.fetchProfileEventFromBigRelaysDataloader.prime(event.pubkey, Promise.resolve(event))
}
async searchNpubsFromCache(query: string, limit: number = 100) {
const result = await this.userIndex.searchAsync(query, { limit })
return result.map((pubkey) => pubkeyToNpub(pubkey as string)).filter(Boolean) as string[]
}
async searchProfilesFromCache(query: string, limit: number = 100) {
const npubs = await this.searchNpubsFromCache(query, limit)
const profiles = await Promise.all(npubs.map((npub) => this.fetchProfile(npub)))
return profiles.filter((profile) => !!profile) as TProfile[]
}
async initUserIndexFromFollowings(pubkey: string, signal: AbortSignal) { async initUserIndexFromFollowings(pubkey: string, signal: AbortSignal) {
const followings = await this.fetchFollowings(pubkey) const followings = await this.fetchFollowings(pubkey)
@ -962,240 +826,336 @@ class ClientService extends EventTarget {
} }
} }
getSeenEventRelays(eventId: string) { /** =========== Profile =========== */
return Array.from(this.pool.seenOn.get(eventId)?.values() || [])
}
getSeenEventRelayUrls(eventId: string) { async searchProfiles(relayUrls: string[], filter: Filter): Promise<TProfile[]> {
return this.getSeenEventRelays(eventId).map((relay) => relay.url) const events = await this.query(relayUrls, {
} ...filter,
kinds: [kinds.Metadata]
})
getEventHints(eventId: string) { const profileEvents = events.sort((a, b) => b.created_at - a.created_at)
return this.getSeenEventRelayUrls(eventId).filter((url) => !isLocalNetworkUrl(url)) await Promise.allSettled(profileEvents.map((profile) => this.addUsernameToIndex(profile)))
profileEvents.forEach((profile) => this.updateProfileEventCache(profile))
return profileEvents.map((profileEvent) => getProfileFromEvent(profileEvent))
} }
getEventHint(eventId: string) { async searchNpubsFromLocal(query: string, limit: number = 100) {
return this.getSeenEventRelayUrls(eventId).find((url) => !isLocalNetworkUrl(url)) ?? '' const result = await this.userIndex.searchAsync(query, { limit })
return result.map((pubkey) => pubkeyToNpub(pubkey as string)).filter(Boolean) as string[]
} }
trackEventSeenOn(eventId: string, relay: AbstractRelay) { async searchProfilesFromLocal(query: string, limit: number = 100) {
let set = this.pool.seenOn.get(eventId) const npubs = await this.searchNpubsFromLocal(query, limit)
if (!set) { const profiles = await Promise.all(npubs.map((npub) => this.fetchProfile(npub)))
set = new Set() return profiles.filter((profile) => !!profile) as TProfile[]
this.pool.seenOn.set(eventId, set)
}
set.add(relay)
} }
private async fetchEventById(relayUrls: string[], id: string): Promise<NEvent | undefined> { private async addUsernameToIndex(profileEvent: NEvent) {
const event = await this.fetchEventFromBigRelaysDataloader.load(id) try {
if (event) { const profileObj = JSON.parse(profileEvent.content)
return event const text = [
} profileObj.display_name?.trim() ?? '',
profileObj.name?.trim() ?? '',
profileObj.nip05
?.split('@')
.map((s: string) => s.trim())
.join(' ') ?? ''
].join(' ')
if (!text) return
return this.tryHarderToFetchEvent(relayUrls, { ids: [id], limit: 1 }, true) await this.userIndex.addAsync(profileEvent.pubkey, text)
} catch {
return
}
} }
private async _fetchEvent(id: string): Promise<NEvent | undefined> { async fetchProfileEvent(id: string, skipCache: boolean = false): Promise<NEvent | undefined> {
let filter: Filter | undefined let pubkey: string | undefined
let relays: string[] = [] let relays: string[] = []
let author: string | undefined
if (/^[0-9a-f]{64}$/.test(id)) { if (/^[0-9a-f]{64}$/.test(id)) {
filter = { ids: [id] } pubkey = id
} else { } else {
const { type, data } = nip19.decode(id) const { data, type } = nip19.decode(id)
switch (type) { switch (type) {
case 'note': case 'npub':
filter = { ids: [data] } pubkey = data
break break
case 'nevent': case 'nprofile':
filter = { ids: [data.id] } pubkey = data.pubkey
if (data.relays) relays = data.relays if (data.relays) relays = data.relays
if (data.author) author = data.author
break break
case 'naddr':
filter = {
authors: [data.pubkey],
kinds: [data.kind],
limit: 1
} }
author = data.pubkey
if (data.identifier) {
filter['#d'] = [data.identifier]
} }
if (data.relays) relays = data.relays
if (!pubkey) {
throw new Error('Invalid id')
}
if (!skipCache) {
const localProfile = await indexedDb.getReplaceableEvent(pubkey, kinds.Metadata)
if (localProfile) {
return localProfile
} }
} }
if (!filter) { const profileFromBigRelays = await this.replaceableEventFromBigRelaysDataloader.load({
throw new Error('Invalid id') pubkey,
kind: kinds.Metadata
})
if (profileFromBigRelays) {
this.addUsernameToIndex(profileFromBigRelays)
return profileFromBigRelays
} }
let event: NEvent | undefined if (!relays.length) {
if (filter.ids) { return undefined
event = await this.fetchEventById(relays, filter.ids[0])
} else {
if (author) {
const relayList = await this.fetchRelayList(author)
relays.push(...relayList.write.slice(0, 4))
} }
event = await this.tryHarderToFetchEvent(relays, filter)
const profileEvent = await this.tryHarderToFetchEvent(
relays,
{
authors: [pubkey],
kinds: [kinds.Metadata],
limit: 1
},
true
)
if (profileEvent) {
this.addUsernameToIndex(profileEvent)
indexedDb.putReplaceableEvent(profileEvent)
} }
if (event && event.id !== id) { return profileEvent
this.eventDataLoader.prime(event.id, Promise.resolve(event))
} }
return event async fetchProfile(id: string, skipCache: boolean = false): Promise<TProfile | undefined> {
const profileEvent = await this.fetchProfileEvent(id, skipCache)
if (profileEvent) {
return getProfileFromEvent(profileEvent)
} }
private async addUsernameToIndex(profileEvent: NEvent) {
try { try {
const profileObj = JSON.parse(profileEvent.content) const pubkey = userIdToPubkey(id)
const text = [ return { pubkey, npub: pubkeyToNpub(pubkey) ?? '', username: formatPubkey(pubkey) }
profileObj.display_name?.trim() ?? '',
profileObj.name?.trim() ?? '',
profileObj.nip05
?.split('@')
.map((s: string) => s.trim())
.join(' ') ?? ''
].join(' ')
if (!text) return
await this.userIndex.addAsync(profileEvent.pubkey, text)
} catch { } catch {
return return undefined
} }
} }
private async tryHarderToFetchEvent( async updateProfileEventCache(event: NEvent) {
relayUrls: string[], await this.updateReplaceableEventFromBigRelaysCache(event)
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.query(relayUrls, filter) /** =========== Relay list =========== */
return events.sort((a, b) => b.created_at - a.created_at)[0]
async fetchRelayListEvent(pubkey: string) {
const [relayEvent] = await this.fetchReplaceableEventsFromBigRelays([pubkey], kinds.RelayList)
return relayEvent ?? null
} }
private async fetchEventsFromBigRelays(ids: readonly string[]) { async fetchRelayList(pubkey: string): Promise<TRelayList> {
const events = await this.query(BIG_RELAY_URLS, { const [relayList] = await this.fetchRelayLists([pubkey])
ids: Array.from(new Set(ids)), return relayList
limit: ids.length }
async fetchRelayLists(pubkeys: string[]): Promise<TRelayList[]> {
const relayEvents = await this.fetchReplaceableEventsFromBigRelays(pubkeys, kinds.RelayList)
return relayEvents.map((event) => {
if (event) {
return getRelayListFromEvent(event)
}
return {
write: BIG_RELAY_URLS,
read: BIG_RELAY_URLS,
originalRelays: []
}
}) })
const eventsMap = new Map<string, NEvent>()
for (const event of events) {
eventsMap.set(event.id, event)
} }
return ids.map((id) => eventsMap.get(id)) async forceUpdateRelayListEvent(pubkey: string) {
await this.replaceableEventBatchLoadFn([{ pubkey, kind: kinds.RelayList }])
} }
private async profileEventBatchLoadFn(pubkeys: readonly string[]) { async updateRelayListCache(event: NEvent) {
const events = await this.query(BIG_RELAY_URLS, { await this.updateReplaceableEventFromBigRelaysCache(event)
authors: Array.from(new Set(pubkeys)), }
kinds: [kinds.Metadata],
limit: pubkeys.length /** =========== Replaceable event from big relays dataloader =========== */
private replaceableEventFromBigRelaysDataloader = new DataLoader<
{ pubkey: string; kind: number },
NEvent | null,
string
>(this.replaceableEventFromBigRelaysBatchLoadFn.bind(this), {
batchScheduleFn: (callback) => setTimeout(callback, 50),
maxBatchSize: 500,
cacheKeyFn: ({ pubkey, kind }) => `${pubkey}:${kind}`
})
private async replaceableEventFromBigRelaysBatchLoadFn(
params: readonly { pubkey: string; kind: number }[]
) {
const groups = new Map<number, string[]>()
params.forEach(({ pubkey, kind }) => {
if (!groups.has(kind)) {
groups.set(kind, [])
}
groups.get(kind)!.push(pubkey)
}) })
const eventsMap = new Map<string, NEvent>() const eventsMap = new Map<string, NEvent>()
await Promise.allSettled(
Array.from(groups.entries()).map(async ([kind, pubkeys]) => {
const events = await this.query(BIG_RELAY_URLS, {
authors: pubkeys,
kinds: [kind]
})
for (const event of events) { for (const event of events) {
const pubkey = event.pubkey const key = `${event.pubkey}:${event.kind}`
const existing = eventsMap.get(pubkey) const existing = eventsMap.get(key)
if (!existing || existing.created_at < event.created_at) { if (!existing || existing.created_at < event.created_at) {
eventsMap.set(pubkey, event) eventsMap.set(key, event)
} }
} }
const profileEvents = pubkeys.map((pubkey) => {
return eventsMap.get(pubkey)
}) })
profileEvents.forEach(
(profileEvent) => profileEvent && indexedDb.putReplaceableEvent(profileEvent)
) )
return profileEvents
return params.map(({ pubkey, kind }) => {
const key = `${pubkey}:${kind}`
const event = eventsMap.get(key)
if (event) {
indexedDb.putReplaceableEvent(event)
return event
} else {
indexedDb.putNullReplaceableEvent(pubkey, kind)
return null
}
})
} }
private async followListEventBatchLoadFn(pubkeys: readonly string[]) { private async fetchReplaceableEventsFromBigRelays(pubkeys: string[], kind: number) {
const events = await this.query(BIG_RELAY_URLS, { const events = await indexedDb.getManyReplaceableEvents(pubkeys, kind)
authors: Array.from(new Set(pubkeys)), const nonExistingPubkeyIndexMap = new Map<string, number>()
kinds: [kinds.Contacts], pubkeys.forEach((pubkey, i) => {
limit: pubkeys.length if (events[i] === undefined) {
nonExistingPubkeyIndexMap.set(pubkey, i)
}
}) })
const eventsMap = new Map<string, NEvent>() const newEvents = await this.replaceableEventFromBigRelaysDataloader.loadMany(
for (const event of events) { Array.from(nonExistingPubkeyIndexMap.keys()).map((pubkey) => ({ pubkey, kind }))
const pubkey = event.pubkey )
const existing = eventsMap.get(pubkey) newEvents.forEach((event) => {
if (!existing || existing.created_at < event.created_at) { if (event && !(event instanceof Error)) {
eventsMap.set(pubkey, event) const index = nonExistingPubkeyIndexMap.get(event.pubkey)
if (index !== undefined) {
events[index] = event
} }
} }
const followListEvents = pubkeys.map((pubkey) => {
return eventsMap.get(pubkey)
}) })
followListEvents.forEach( return events
(followListEvent) => followListEvent && indexedDb.putReplaceableEvent(followListEvent) }
private async updateReplaceableEventFromBigRelaysCache(event: NEvent) {
this.replaceableEventFromBigRelaysDataloader.clear({ pubkey: event.pubkey, kind: event.kind })
this.replaceableEventFromBigRelaysDataloader.prime(
{ pubkey: event.pubkey, kind: event.kind },
Promise.resolve(event)
) )
return followListEvents await indexedDb.putReplaceableEvent(event)
} }
private async relayListEventBatchLoadFn(pubkeys: readonly string[]) { /** =========== Replaceable event dataloader =========== */
const events = await this.query(BIG_RELAY_URLS, {
authors: pubkeys as string[], private replaceableEventDataLoader = new DataLoader<
kinds: [kinds.RelayList], { pubkey: string; kind: number },
limit: pubkeys.length NEvent | null,
string
>(this.replaceableEventBatchLoadFn.bind(this), {
cacheKeyFn: ({ pubkey, kind }) => `${pubkey}:${kind}`
}) })
const eventsMap = new Map<string, NEvent>()
for (const event of events) { private async replaceableEventBatchLoadFn(params: readonly { pubkey: string; kind: number }[]) {
const pubkey = event.pubkey const results = await Promise.allSettled(
const existing = eventsMap.get(pubkey) params.map(async ({ pubkey, kind }) => {
if (!existing || existing.created_at < event.created_at) { const relayList = await this.fetchRelayList(pubkey)
eventsMap.set(pubkey, event) const events = await this.query(relayList.write.concat(BIG_RELAY_URLS).slice(0, 5), {
} authors: [pubkey],
} kinds: [kind]
pubkeys.forEach((pubkey) => { })
const event = eventsMap.get(pubkey) const event = getLatestEvent(events) ?? null
if (event) { if (event) {
indexedDb.putReplaceableEvent(event) indexedDb.putReplaceableEvent(event)
} else { } else {
indexedDb.putNullReplaceableEvent(pubkey, kinds.RelayList) indexedDb.putNullReplaceableEvent(pubkey, kind)
}
return event
})
)
return results.map((result) => {
if (result.status === 'fulfilled') {
return result.value
} else {
console.error('Failed to load replaceable event:', result.reason)
return null
} }
}) })
}
return pubkeys.map((pubkey) => eventsMap.get(pubkey)) private async fetchReplaceableEvent(pubkey: string, kind: number) {
const storedEvent = await indexedDb.getReplaceableEvent(pubkey, kind)
if (storedEvent !== undefined) {
return storedEvent
} }
private async _fetchFollowListEvent(pubkey: string) { return await this.replaceableEventDataLoader.load({ pubkey, kind })
const storedFollowListEvent = await indexedDb.getReplaceableEvent(pubkey, kinds.Contacts)
if (storedFollowListEvent) {
return storedFollowListEvent
} }
const followListEventFromBigRelays =
await this.fetchFollowListEventFromBigRelaysDataloader.load(pubkey) private async updateReplaceableEventCache(event: NEvent) {
if (followListEventFromBigRelays) { this.replaceableEventDataLoader.clear({ pubkey: event.pubkey, kind: event.kind })
return followListEventFromBigRelays this.replaceableEventDataLoader.prime(
{ pubkey: event.pubkey, kind: event.kind },
Promise.resolve(event)
)
await indexedDb.putReplaceableEvent(event)
} }
const relayList = await this.fetchRelayList(pubkey) /** =========== Replaceable event =========== */
const relays = relayList.write.filter((url) => !BIG_RELAY_URLS.includes(url))
if (!relays.length) { async fetchFollowListEvent(pubkey: string) {
return undefined return await this.fetchReplaceableEvent(pubkey, kinds.Contacts)
} }
const followListEvents = await this.query(relays, { async fetchFollowings(pubkey: string) {
authors: [pubkey], const followListEvent = await this.fetchFollowListEvent(pubkey)
kinds: [kinds.Contacts] return followListEvent ? getPubkeysFromPTags(followListEvent.tags) : []
}) }
async updateFollowListCache(evt: NEvent) {
await this.updateReplaceableEventCache(evt)
}
async fetchMuteListEvent(pubkey: string) {
return await this.fetchReplaceableEvent(pubkey, kinds.Mutelist)
}
return followListEvents.sort((a, b) => b.created_at - a.created_at)[0] async fetchBookmarkListEvent(pubkey: string) {
return this.fetchReplaceableEvent(pubkey, kinds.BookmarkList)
}
async fetchBlossomServerListEvent(pubkey: string) {
return await this.fetchReplaceableEvent(pubkey, ExtendedKind.BLOSSOM_SERVER_LIST)
}
async fetchBlossomServerList(pubkey: string) {
const evt = await this.fetchBlossomServerListEvent(pubkey)
return evt ? getServersFromServerTags(evt.tags) : []
}
async updateBlossomServerListEventCache(evt: NEvent) {
await this.updateReplaceableEventCache(evt)
} }
} }

2
src/services/indexed-db.service.ts

@ -473,7 +473,7 @@ class IndexedDbService {
}, },
{ {
name: StoreNames.BLOSSOM_SERVER_LIST_EVENTS, name: StoreNames.BLOSSOM_SERVER_LIST_EVENTS,
expirationTimestamp: Date.now() - 1000 * 60 * 60 * 24 * 7 // 7 days expirationTimestamp: Date.now() - 1000 * 60 * 60 * 24 // 1 days
} }
] ]
const transaction = this.db!.transaction( const transaction = this.db!.transaction(

Loading…
Cancel
Save