diff --git a/src/components/PostEditor/PostTextarea/suggestion.ts b/src/components/PostEditor/PostTextarea/suggestion.ts index 7f1336a..beb42aa 100644 --- a/src/components/PostEditor/PostTextarea/suggestion.ts +++ b/src/components/PostEditor/PostTextarea/suggestion.ts @@ -8,7 +8,7 @@ import MentionList, { MentionListHandle, MentionListProps } from './MentionList' const suggestion = { items: async ({ query }: { query: string }) => { - return await client.searchNpubsFromCache(query, 20) + return await client.searchNpubsFromLocal(query, 20) }, render: () => { diff --git a/src/hooks/useSearchProfiles.tsx b/src/hooks/useSearchProfiles.tsx index f930fff..512e4c6 100644 --- a/src/hooks/useSearchProfiles.tsx +++ b/src/hooks/useSearchProfiles.tsx @@ -22,7 +22,7 @@ export function useSearchProfiles(search: string, limit: number) { setIsFetching(true) setProfiles([]) try { - const profiles = await client.searchProfilesFromCache(search, limit) + const profiles = await client.searchProfilesFromLocal(search, limit) setProfiles(profiles) if (profiles.length >= limit) { return diff --git a/src/lib/event-metadata.ts b/src/lib/event-metadata.ts index 9c9f0e9..5457cfd 100644 --- a/src/lib/event-metadata.ts +++ b/src/lib/event-metadata.ts @@ -8,7 +8,7 @@ import { generateBech32IdFromETag, tagNameEquals } from './tag' import { isWebsocketUrl, normalizeHttpUrl, normalizeUrl } from './url' import { isTorBrowser } from './utils' -export function getRelayListFromEvent(event?: Event) { +export function getRelayListFromEvent(event?: Event | null) { if (!event) { return { write: BIG_RELAY_URLS, read: BIG_RELAY_URLS, originalRelays: [] } } diff --git a/src/lib/event.ts b/src/lib/event.ts index 32e8006..7d7dcd3 100644 --- a/src/lib/event.ts +++ b/src/lib/event.ts @@ -222,7 +222,7 @@ export function getEmbeddedPubkeys(event: Event) { 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] } diff --git a/src/providers/NostrProvider/index.tsx b/src/providers/NostrProvider/index.tsx index 1bea41b..f24015c 100644 --- a/src/providers/NostrProvider/index.tsx +++ b/src/providers/NostrProvider/index.tsx @@ -650,7 +650,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { if (newFollowListEvent.id !== followListEvent.id) return setFollowListEvent(newFollowListEvent) - client.updateFollowListCache(newFollowListEvent) + await client.updateFollowListCache(newFollowListEvent) } const updateMuteListEvent = async (muteListEvent: Event, privateTags: string[][]) => { diff --git a/src/providers/UserTrustProvider.tsx b/src/providers/UserTrustProvider.tsx index 9260884..e01d212 100644 --- a/src/providers/UserTrustProvider.tsx +++ b/src/providers/UserTrustProvider.tsx @@ -42,13 +42,21 @@ export function UserTrustProvider({ children }: { children: React.ReactNode }) { const initWoT = async () => { const followings = await client.fetchFollowings(currentPubkey) - await Promise.allSettled( - followings.map(async (pubkey) => { - wotSet.add(pubkey) - const _followings = await client.fetchFollowings(pubkey) - _followings.forEach((following) => wotSet.add(following)) - }) - ) + 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( + batch.map(async (pubkey) => { + const _followings = await client.fetchFollowings(pubkey) + _followings.forEach((following) => { + wotSet.add(following) + }) + }) + ) + await new Promise((resolve) => setTimeout(resolve, 200)) + } } initWoT() }, [currentPubkey]) diff --git a/src/services/client.service.ts b/src/services/client.service.ts index da245dd..56570e5 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -1,4 +1,5 @@ import { BIG_RELAY_URLS, ExtendedKind } from '@/constants' +import { getLatestEvent } from '@/lib/event' import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata' import { formatPubkey, pubkeyToNpub, userIdToPubkey } from '@/lib/pubkey' import { getPubkeysFromPTags, getServersFromServerTags } from '@/lib/tag' @@ -50,39 +51,6 @@ class ClientService extends EventTarget { this.fetchEventsFromBigRelays.bind(this), { cache: false, batchScheduleFn: (callback) => setTimeout(callback, 50) } ) - private fetchProfileEventFromBigRelaysDataloader = new DataLoader( - this.profileEventBatchLoadFn.bind(this), - { - batchScheduleFn: (callback) => setTimeout(callback, 50), - maxBatchSize: 500 - } - ) - private fetchFollowListEventFromBigRelaysDataloader = new DataLoader( - this.followListEventBatchLoadFn.bind(this), - { - batchScheduleFn: (callback) => setTimeout(callback, 50), - maxBatchSize: 500 - } - ) - private relayListEventDataLoader = new DataLoader( - this.relayListEventBatchLoadFn.bind(this), - { - batchScheduleFn: (callback) => setTimeout(callback, 50), - maxBatchSize: 500 - } - ) - private followListCache = new LRUCache>({ - max: 2000, - fetchMethod: this._fetchFollowListEvent.bind(this) - }) - private followingFavoriteRelaysCache = new LRUCache>({ - max: 10, - fetchMethod: this._fetchFollowingFavoriteRelays.bind(this) - }) - private blossomServerListEventCache = new LRUCache>({ - max: 1000, - fetchMethod: this._fetchBlossomServerListEvent.bind(this) - }) private userIndex = new FlexSearch.Index({ tokenize: 'forward' @@ -169,6 +137,8 @@ class ClientService extends EventTarget { return 'Nostr ' + btoa(JSON.stringify(event)) } + /** =========== Timeline =========== */ + private generateTimelineKey(urls: string[], filter: Filter) { const stableFilter: any = {} 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((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( urls: string[], filter: Omit & { limit: number }, // filter with limit, @@ -610,6 +559,54 @@ class ClientService extends EventTarget { 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((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( urls: string[], filter: Filter | Filter[], @@ -661,175 +658,108 @@ class ClientService extends EventTarget { this.eventDataLoader.prime(event.id, Promise.resolve(event)) } - async fetchProfileEvent(id: string, skipCache: boolean = false): Promise { - let pubkey: string | undefined + 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 _fetchEvent(id: string): Promise { + let filter: Filter | undefined let relays: string[] = [] + let author: string | undefined if (/^[0-9a-f]{64}$/.test(id)) { - pubkey = id + filter = { ids: [id] } } else { - const { data, type } = nip19.decode(id) + const { type, data } = nip19.decode(id) switch (type) { - case 'npub': - pubkey = data + case 'note': + filter = { ids: [data] } break - case 'nprofile': - pubkey = data.pubkey + case 'nevent': + filter = { ids: [data.id] } if (data.relays) relays = data.relays + if (data.author) author = data.author 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) { + if (!filter) { 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 + let event: NEvent | undefined + if (filter.ids) { + 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) { + this.eventDataLoader.prime(event.id, Promise.resolve(event)) } - return profileEvent + return event } - async fetchProfile(id: string, skipCache: boolean = false): Promise { - const profileEvent = await this.fetchProfileEvent(id, skipCache) - if (profileEvent) { - return getProfileFromEvent(profileEvent) - } - - try { - const pubkey = userIdToPubkey(id) - return { pubkey, npub: pubkeyToNpub(pubkey) ?? '', username: formatPubkey(pubkey) } - } catch { - return undefined + 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 } - } - - async searchProfiles(relayUrls: string[], filter: Filter): Promise { - const events = await this.query(relayUrls, { - ...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 this.relayListEventDataLoader.load(pubkey) - } + if (!relayUrls.length) return - async fetchRelayList(pubkey: string): Promise { - const [relayList] = await this.fetchRelayLists([pubkey]) - return relayList + const events = await this.query(relayUrls, filter) + return events.sort((a, b) => b.created_at - a.created_at)[0] } - async fetchRelayLists(pubkeys: string[]): Promise { - const relayEvents = await indexedDb.getManyReplaceableEvents(pubkeys, kinds.RelayList) - const nonExistingPubkeyIndexMap = new Map() - 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 - } - } + private async fetchEventsFromBigRelays(ids: readonly string[]) { + const events = await this.query(BIG_RELAY_URLS, { + ids: Array.from(new Set(ids)), + limit: ids.length }) + const eventsMap = new Map() + for (const event of events) { + eventsMap.set(event.id, event) + } - return relayEvents.map((event) => { - if (event) { - return getRelayListFromEvent(event) - } - return { - write: BIG_RELAY_URLS, - read: BIG_RELAY_URLS, - originalRelays: [] - } - }) + return ids.map((id) => eventsMap.get(id)) } - async forceUpdateRelayListEvent(pubkey: string) { - await this.relayListEventBatchLoadFn([pubkey]) - } + /** =========== Following favorite relays =========== */ - async fetchFollowListEvent(pubkey: string) { - return await this.followListCache.fetch(pubkey) - } + private followingFavoriteRelaysCache = new LRUCache>({ + max: 10, + fetchMethod: this._fetchFollowingFavoriteRelays.bind(this) + }) - async fetchMuteListEvent(pubkey: string): Promise { - const storedEvent = await indexedDb.getReplaceableEvent(pubkey, kinds.Mutelist) - if (storedEvent) { - 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 { - const storedBookmarkListEvent = await indexedDb.getReplaceableEvent(pubkey, kinds.BookmarkList) - 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) + async fetchFollowingFavoriteRelays(pubkey: string) { + return this.followingFavoriteRelaysCache.fetch(pubkey) } private async _fetchFollowingFavoriteRelays(pubkey: string) { @@ -883,73 +813,7 @@ class ClientService extends EventTarget { return fetchNewData() } - async fetchBlossomServerList(pubkey: string) { - 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[] - } + /** =========== Followings =========== */ async initUserIndexFromFollowings(pubkey: string, signal: AbortSignal) { const followings = await this.fetchFollowings(pubkey) @@ -962,240 +826,336 @@ class ClientService extends EventTarget { } } - getSeenEventRelays(eventId: string) { - return Array.from(this.pool.seenOn.get(eventId)?.values() || []) - } + /** =========== Profile =========== */ - getSeenEventRelayUrls(eventId: string) { - return this.getSeenEventRelays(eventId).map((relay) => relay.url) - } + async searchProfiles(relayUrls: string[], filter: Filter): Promise { + const events = await this.query(relayUrls, { + ...filter, + kinds: [kinds.Metadata] + }) - getEventHints(eventId: string) { - return this.getSeenEventRelayUrls(eventId).filter((url) => !isLocalNetworkUrl(url)) + 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)) } - getEventHint(eventId: string) { - return this.getSeenEventRelayUrls(eventId).find((url) => !isLocalNetworkUrl(url)) ?? '' + async searchNpubsFromLocal(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[] } - 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) + async searchProfilesFromLocal(query: string, limit: number = 100) { + const npubs = await this.searchNpubsFromLocal(query, limit) + const profiles = await Promise.all(npubs.map((npub) => this.fetchProfile(npub))) + return profiles.filter((profile) => !!profile) as TProfile[] } - private async fetchEventById(relayUrls: string[], id: string): Promise { - const event = await this.fetchEventFromBigRelaysDataloader.load(id) - if (event) { - return event - } + private async addUsernameToIndex(profileEvent: NEvent) { + try { + const profileObj = JSON.parse(profileEvent.content) + 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 { - let filter: Filter | undefined + async fetchProfileEvent(id: string, skipCache: boolean = false): Promise { + let pubkey: string | undefined let relays: string[] = [] - let author: string | undefined if (/^[0-9a-f]{64}$/.test(id)) { - filter = { ids: [id] } + pubkey = id } else { - const { type, data } = nip19.decode(id) + const { data, type } = nip19.decode(id) switch (type) { - case 'note': - filter = { ids: [data] } + case 'npub': + pubkey = data break - case 'nevent': - filter = { ids: [data.id] } + case 'nprofile': + pubkey = data.pubkey if (data.relays) relays = data.relays - if (data.author) author = data.author 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 (!filter) { + + if (!pubkey) { throw new Error('Invalid id') } - - let event: NEvent | undefined - if (filter.ids) { - event = await this.fetchEventById(relays, filter.ids[0]) - } else { - if (author) { - const relayList = await this.fetchRelayList(author) - relays.push(...relayList.write.slice(0, 4)) + if (!skipCache) { + const localProfile = await indexedDb.getReplaceableEvent(pubkey, kinds.Metadata) + if (localProfile) { + return localProfile } - event = await this.tryHarderToFetchEvent(relays, filter) + } + const profileFromBigRelays = await this.replaceableEventFromBigRelaysDataloader.load({ + pubkey, + kind: kinds.Metadata + }) + if (profileFromBigRelays) { + this.addUsernameToIndex(profileFromBigRelays) + return profileFromBigRelays } - if (event && event.id !== id) { - this.eventDataLoader.prime(event.id, Promise.resolve(event)) + if (!relays.length) { + return undefined } - return event + const profileEvent = await this.tryHarderToFetchEvent( + relays, + { + authors: [pubkey], + kinds: [kinds.Metadata], + limit: 1 + }, + true + ) + + if (profileEvent) { + this.addUsernameToIndex(profileEvent) + indexedDb.putReplaceableEvent(profileEvent) + } + + return profileEvent } - private async addUsernameToIndex(profileEvent: NEvent) { - try { - const profileObj = JSON.parse(profileEvent.content) - const text = [ - profileObj.display_name?.trim() ?? '', - profileObj.name?.trim() ?? '', - profileObj.nip05 - ?.split('@') - .map((s: string) => s.trim()) - .join(' ') ?? '' - ].join(' ') - if (!text) return + async fetchProfile(id: string, skipCache: boolean = false): Promise { + const profileEvent = await this.fetchProfileEvent(id, skipCache) + if (profileEvent) { + return getProfileFromEvent(profileEvent) + } - await this.userIndex.addAsync(profileEvent.pubkey, text) + try { + const pubkey = userIdToPubkey(id) + return { pubkey, npub: pubkeyToNpub(pubkey) ?? '', username: formatPubkey(pubkey) } } catch { - return + return undefined } } - 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 + async updateProfileEventCache(event: NEvent) { + await this.updateReplaceableEventFromBigRelaysCache(event) + } - const events = await this.query(relayUrls, filter) - return events.sort((a, b) => b.created_at - a.created_at)[0] + /** =========== Relay list =========== */ + + async fetchRelayListEvent(pubkey: string) { + const [relayEvent] = await this.fetchReplaceableEventsFromBigRelays([pubkey], kinds.RelayList) + return relayEvent ?? null } - private async fetchEventsFromBigRelays(ids: readonly string[]) { - const events = await this.query(BIG_RELAY_URLS, { - ids: Array.from(new Set(ids)), - limit: ids.length + async fetchRelayList(pubkey: string): Promise { + const [relayList] = await this.fetchRelayLists([pubkey]) + return relayList + } + + async fetchRelayLists(pubkeys: string[]): Promise { + 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() - 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[]) { - const events = await this.query(BIG_RELAY_URLS, { - authors: Array.from(new Set(pubkeys)), - kinds: [kinds.Metadata], - limit: pubkeys.length - }) - const eventsMap = new Map() - for (const event of events) { - const pubkey = event.pubkey - const existing = eventsMap.get(pubkey) - if (!existing || existing.created_at < event.created_at) { - eventsMap.set(pubkey, event) + async updateRelayListCache(event: NEvent) { + await this.updateReplaceableEventFromBigRelaysCache(event) + } + + /** =========== 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() + params.forEach(({ pubkey, kind }) => { + if (!groups.has(kind)) { + groups.set(kind, []) } - } - const profileEvents = pubkeys.map((pubkey) => { - return eventsMap.get(pubkey) + groups.get(kind)!.push(pubkey) }) - profileEvents.forEach( - (profileEvent) => profileEvent && indexedDb.putReplaceableEvent(profileEvent) + const eventsMap = new Map() + 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) { + const key = `${event.pubkey}:${event.kind}` + const existing = eventsMap.get(key) + if (!existing || existing.created_at < event.created_at) { + eventsMap.set(key, event) + } + } + }) ) - 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[]) { - const events = await this.query(BIG_RELAY_URLS, { - authors: Array.from(new Set(pubkeys)), - kinds: [kinds.Contacts], - limit: pubkeys.length + private async fetchReplaceableEventsFromBigRelays(pubkeys: string[], kind: number) { + const events = await indexedDb.getManyReplaceableEvents(pubkeys, kind) + const nonExistingPubkeyIndexMap = new Map() + pubkeys.forEach((pubkey, i) => { + if (events[i] === undefined) { + nonExistingPubkeyIndexMap.set(pubkey, i) + } }) - const eventsMap = new Map() - for (const event of events) { - const pubkey = event.pubkey - const existing = eventsMap.get(pubkey) - if (!existing || existing.created_at < event.created_at) { - eventsMap.set(pubkey, event) + const newEvents = await this.replaceableEventFromBigRelaysDataloader.loadMany( + Array.from(nonExistingPubkeyIndexMap.keys()).map((pubkey) => ({ pubkey, kind })) + ) + newEvents.forEach((event) => { + if (event && !(event instanceof Error)) { + const index = nonExistingPubkeyIndexMap.get(event.pubkey) + if (index !== undefined) { + events[index] = event + } } - } - const followListEvents = pubkeys.map((pubkey) => { - return eventsMap.get(pubkey) }) - followListEvents.forEach( - (followListEvent) => followListEvent && indexedDb.putReplaceableEvent(followListEvent) + return events + } + + 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[]) { - const events = await this.query(BIG_RELAY_URLS, { - authors: pubkeys as string[], - kinds: [kinds.RelayList], - limit: pubkeys.length - }) - const eventsMap = new Map() - for (const event of events) { - const pubkey = event.pubkey - const existing = eventsMap.get(pubkey) - if (!existing || existing.created_at < event.created_at) { - eventsMap.set(pubkey, event) - } - } - pubkeys.forEach((pubkey) => { - const event = eventsMap.get(pubkey) - if (event) { - indexedDb.putReplaceableEvent(event) + /** =========== Replaceable event dataloader =========== */ + + private replaceableEventDataLoader = new DataLoader< + { pubkey: string; kind: number }, + NEvent | null, + string + >(this.replaceableEventBatchLoadFn.bind(this), { + cacheKeyFn: ({ pubkey, kind }) => `${pubkey}:${kind}` + }) + + private async replaceableEventBatchLoadFn(params: readonly { pubkey: string; kind: number }[]) { + const results = await Promise.allSettled( + params.map(async ({ pubkey, kind }) => { + const relayList = await this.fetchRelayList(pubkey) + const events = await this.query(relayList.write.concat(BIG_RELAY_URLS).slice(0, 5), { + authors: [pubkey], + kinds: [kind] + }) + const event = getLatestEvent(events) ?? null + if (event) { + indexedDb.putReplaceableEvent(event) + } else { + indexedDb.putNullReplaceableEvent(pubkey, kind) + } + return event + }) + ) + return results.map((result) => { + if (result.status === 'fulfilled') { + return result.value } else { - indexedDb.putNullReplaceableEvent(pubkey, kinds.RelayList) + console.error('Failed to load replaceable event:', result.reason) + return null } }) - - return pubkeys.map((pubkey) => eventsMap.get(pubkey)) } - private async _fetchFollowListEvent(pubkey: string) { - const storedFollowListEvent = await indexedDb.getReplaceableEvent(pubkey, kinds.Contacts) - if (storedFollowListEvent) { - return storedFollowListEvent - } - const followListEventFromBigRelays = - await this.fetchFollowListEventFromBigRelaysDataloader.load(pubkey) - if (followListEventFromBigRelays) { - return followListEventFromBigRelays + private async fetchReplaceableEvent(pubkey: string, kind: number) { + const storedEvent = await indexedDb.getReplaceableEvent(pubkey, kind) + if (storedEvent !== undefined) { + return storedEvent } - const relayList = await this.fetchRelayList(pubkey) - const relays = relayList.write.filter((url) => !BIG_RELAY_URLS.includes(url)) - if (!relays.length) { - return undefined - } + return await this.replaceableEventDataLoader.load({ pubkey, kind }) + } - const followListEvents = await this.query(relays, { - authors: [pubkey], - kinds: [kinds.Contacts] - }) + private async updateReplaceableEventCache(event: NEvent) { + 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 =========== */ - return followListEvents.sort((a, b) => b.created_at - a.created_at)[0] + async fetchFollowListEvent(pubkey: string) { + return await this.fetchReplaceableEvent(pubkey, kinds.Contacts) + } + + async fetchFollowings(pubkey: string) { + const followListEvent = await this.fetchFollowListEvent(pubkey) + return followListEvent ? getPubkeysFromPTags(followListEvent.tags) : [] + } + + async updateFollowListCache(evt: NEvent) { + await this.updateReplaceableEventCache(evt) + } + + async fetchMuteListEvent(pubkey: string) { + return await this.fetchReplaceableEvent(pubkey, kinds.Mutelist) + } + + 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) } } diff --git a/src/services/indexed-db.service.ts b/src/services/indexed-db.service.ts index ae17058..c72b573 100644 --- a/src/services/indexed-db.service.ts +++ b/src/services/indexed-db.service.ts @@ -473,7 +473,7 @@ class IndexedDbService { }, { 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(