From 984d18683ef35e5d6dd6ad0e23ea0eb0756421f8 Mon Sep 17 00:00:00 2001 From: codytseng Date: Sun, 15 Dec 2024 15:24:30 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=F0=9F=8F=97=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/NoteList/index.tsx | 93 ++-- .../src/components/NotificationList/index.tsx | 55 ++- .../src/components/ReplyNoteList/index.tsx | 58 ++- .../pages/secondary/NoteListPage/index.tsx | 20 +- .../src/providers/NostrProvider/index.tsx | 1 - src/renderer/src/services/client.service.ts | 412 +++++++----------- 6 files changed, 287 insertions(+), 352 deletions(-) diff --git a/src/renderer/src/components/NoteList/index.tsx b/src/renderer/src/components/NoteList/index.tsx index 2a93c73..7ce356f 100644 --- a/src/renderer/src/components/NoteList/index.tsx +++ b/src/renderer/src/components/NoteList/index.tsx @@ -12,6 +12,9 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import NoteCard from '../NoteCard' +const NORMAL_RELAY_LIMIT = 100 +const ALGO_RELAY_LIMIT = 500 + export default function NoteList({ relayUrls, filter = {}, @@ -24,9 +27,9 @@ export default function NoteList({ const { t } = useTranslation() const { isReady, signEvent } = useNostr() const { isFetching: isFetchingRelayInfo, areAlgoRelays } = useFetchRelayInfos(relayUrls) + const [timelineKey, setTimelineKey] = useState(undefined) const [events, setEvents] = useState([]) const [newEvents, setNewEvents] = useState([]) - const [until, setUntil] = useState(() => dayjs().unix()) const [hasMore, setHasMore] = useState(true) const [initialized, setInitialized] = useState(false) const [displayReplies, setDisplayReplies] = useState(false) @@ -35,7 +38,7 @@ export default function NoteList({ const noteFilter = useMemo(() => { return { kinds: [kinds.ShortTextNote, kinds.Repost], - limit: areAlgoRelays ? 500 : 50, + limit: areAlgoRelays ? ALGO_RELAY_LIMIT : NORMAL_RELAY_LIMIT, ...filter } }, [JSON.stringify(filter), areAlgoRelays]) @@ -43,40 +46,44 @@ export default function NoteList({ useEffect(() => { if (!isReady || isFetchingRelayInfo) return - setInitialized(false) - setEvents([]) - setNewEvents([]) - setHasMore(true) - - const subCloser = client.subscribeEventsWithAuth( - relayUrls, - noteFilter, - { - onEose: (events) => { - if (!areAlgoRelays) { - events.sort((a, b) => b.created_at - a.created_at) - } - events = events.slice(0, noteFilter.limit) - if (events.length > 0) { - setEvents((pre) => [...pre, ...events]) - setUntil(events[events.length - 1].created_at - 1) - } else { - setHasMore(false) + async function init() { + setInitialized(false) + setEvents([]) + setNewEvents([]) + setHasMore(true) + + const { closer, timelineKey } = await client.subscribeTimeline( + relayUrls, + noteFilter, + { + onEvents: (events, eosed) => { + if (events.length > 0) { + setEvents(events) + } else { + setHasMore(false) + } + if (areAlgoRelays) { + setHasMore(false) + } + if (eosed) { + setInitialized(true) + } + }, + onNew: (event) => { + setNewEvents((oldEvents) => + [event, ...oldEvents].sort((a, b) => b.created_at - a.created_at) + ) } - if (areAlgoRelays) { - setHasMore(false) - } - setInitialized(true) }, - onNew: (event) => { - setNewEvents((oldEvents) => [event, ...oldEvents]) - } - }, - signEvent - ) + { signer: signEvent, needSort: !areAlgoRelays } + ) + setTimelineKey(timelineKey) + return closer + } + const promise = init() return () => { - subCloser() + promise.then((closer) => closer()) } }, [ JSON.stringify(relayUrls), @@ -110,23 +117,21 @@ export default function NoteList({ observer.current.unobserve(bottomRef.current) } } - }, [until, initialized, hasMore]) + }, [initialized, hasMore, events, timelineKey]) const loadMore = async () => { - const events = await client.fetchEvents(relayUrls, { ...noteFilter, until }, true) - const sortedEvents = events - .sort((a, b) => b.created_at - a.created_at) - .slice(0, noteFilter.limit) - if (sortedEvents.length === 0) { + if (!timelineKey) return + + const newEvents = await client.loadMoreTimeline( + timelineKey, + events.length ? events[events.length - 1].created_at - 1 : dayjs().unix(), + noteFilter.limit + ) + if (newEvents.length === 0) { setHasMore(false) return } - - if (sortedEvents.length > 0) { - setEvents((oldEvents) => [...oldEvents, ...sortedEvents]) - } - - setUntil(sortedEvents[sortedEvents.length - 1].created_at - 1) + setEvents((oldEvents) => [...oldEvents, ...newEvents]) } const showNewEvents = () => { diff --git a/src/renderer/src/components/NotificationList/index.tsx b/src/renderer/src/components/NotificationList/index.tsx index cf4ba9e..5f3f4c7 100644 --- a/src/renderer/src/components/NotificationList/index.tsx +++ b/src/renderer/src/components/NotificationList/index.tsx @@ -17,35 +17,46 @@ const LIMIT = 50 export default function NotificationList() { const { t } = useTranslation() const { pubkey } = useNostr() + const [timelineKey, setTimelineKey] = useState(undefined) const [initialized, setInitialized] = useState(false) const [notifications, setNotifications] = useState([]) - const [until, setUntil] = useState(dayjs().unix()) + const [until, setUntil] = useState(dayjs().unix()) const bottomRef = useRef(null) const observer = useRef(null) - const [hasMore, setHasMore] = useState(true) useEffect(() => { if (!pubkey) { - setHasMore(false) + setUntil(undefined) return } const init = async () => { - setHasMore(true) - const subCloser = await client.subscribeNotifications(pubkey, LIMIT, { - onNotifications: (events, isCache) => { - setNotifications(events) - setUntil(events.length ? events[events.length - 1].created_at - 1 : dayjs().unix()) - if (!isCache) { - setInitialized(true) - } + const relayList = await client.fetchRelayList(pubkey) + const { closer, timelineKey } = await client.subscribeTimeline( + relayList.read.length >= 4 + ? relayList.read + : relayList.read.concat(client.getDefaultRelayUrls()).slice(0, 4), + { + '#p': [pubkey], + kinds: [kinds.ShortTextNote, kinds.Repost, kinds.Reaction], + limit: LIMIT }, - onNew: (event) => { - setNotifications((oldEvents) => [event, ...oldEvents]) + { + onEvents: (events, eosed) => { + setNotifications(events.filter((event) => event.pubkey !== pubkey)) + setUntil(events.length >= LIMIT ? events[events.length - 1].created_at - 1 : undefined) + if (eosed) { + setInitialized(true) + } + }, + onNew: (event) => { + if (event.pubkey === pubkey) return + setNotifications((oldEvents) => [event, ...oldEvents]) + } } - }) - - return subCloser + ) + setTimelineKey(timelineKey) + return closer } const promise = init() @@ -64,7 +75,7 @@ export default function NotificationList() { } observer.current = new IntersectionObserver((entries) => { - if (entries[0].isIntersecting && hasMore) { + if (entries[0].isIntersecting) { loadMore() } }, options) @@ -78,13 +89,13 @@ export default function NotificationList() { observer.current.unobserve(bottomRef.current) } } - }, [until, initialized, hasMore]) + }, [until, initialized, timelineKey]) const loadMore = async () => { - if (!pubkey) return - const notifications = await client.fetchMoreNotifications(pubkey, until, LIMIT) + if (!pubkey || !timelineKey || !until) return + const notifications = await client.loadMoreTimeline(timelineKey, until, LIMIT) if (notifications.length === 0) { - setHasMore(false) + setUntil(undefined) return } @@ -101,7 +112,7 @@ export default function NotificationList() { ))}
- {hasMore ?
{t('loading...')}
: t('no more notifications')} + {until ?
{t('loading...')}
: t('no more notifications')}
) diff --git a/src/renderer/src/components/ReplyNoteList/index.tsx b/src/renderer/src/components/ReplyNoteList/index.tsx index bce6e46..10669fb 100644 --- a/src/renderer/src/components/ReplyNoteList/index.tsx +++ b/src/renderer/src/components/ReplyNoteList/index.tsx @@ -1,22 +1,27 @@ import { Separator } from '@renderer/components/ui/separator' +import { isReplyNoteEvent } from '@renderer/lib/event' import { isReplyETag, isRootETag } from '@renderer/lib/tag' import { cn } from '@renderer/lib/utils' import { useNostr } from '@renderer/providers/NostrProvider' import { useNoteStats } from '@renderer/providers/NoteStatsProvider' import client from '@renderer/services/client.service' -import { Event } from 'nostr-tools' +import dayjs from 'dayjs' +import { Event, kinds } from 'nostr-tools' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import ReplyNote from '../ReplyNote' +const LIMIT = 100 + export default function ReplyNoteList({ event, className }: { event: Event; className?: string }) { const { t } = useTranslation() const { isReady, pubkey } = useNostr() + const [timelineKey, setTimelineKey] = useState(undefined) + const [until, setUntil] = useState(() => dayjs().unix()) const [replies, setReplies] = useState([]) const [replyMap, setReplyMap] = useState< Record >({}) - const [until, setUntil] = useState() const [loading, setLoading] = useState(false) const [highlightReplyId, setHighlightReplyId] = useState(undefined) const { updateNoteReplyCount } = useNoteStats() @@ -28,25 +33,35 @@ export default function ReplyNoteList({ event, className }: { event: Event; clas const init = async () => { setLoading(true) setReplies([]) - setUntil(undefined) try { const relayList = await client.fetchRelayList(event.pubkey) - const closer = await client.subscribeReplies(relayList.read.slice(0, 5), event.id, 100, { - onReplies: (evts, isCache, until) => { - setReplies(evts) - setUntil(until) - if (!isCache) { - setLoading(false) - } + const { closer, timelineKey } = await client.subscribeTimeline( + relayList.read.slice(0, 5), + { + '#e': [event.id], + kinds: [kinds.ShortTextNote], + limit: LIMIT }, - onNew: (evt) => { - setReplies((pre) => [...pre, evt]) - if (evt.pubkey === pubkey) { - highlightReply(evt.id) + { + onEvents: (evts, eosed) => { + setReplies(evts.filter((evt) => isReplyNoteEvent(evt)).reverse()) + if (eosed) { + setLoading(false) + setUntil(evts.length >= LIMIT ? evts[evts.length - 1].created_at - 1 : undefined) + } + }, + onNew: (evt) => { + if (!isReplyNoteEvent(evt)) return + + setReplies((pre) => [...pre, evt]) + if (evt.pubkey === pubkey) { + highlightReply(evt.id) + } } } - }) + ) + setTimelineKey(timelineKey) return closer } catch { setLoading(false) @@ -96,20 +111,15 @@ export default function ReplyNoteList({ event, className }: { event: Event; clas }, [replies]) const loadMore = async () => { - if (loading || !until) return + if (loading || !until || !timelineKey) return setLoading(true) - const relayList = await client.fetchRelayList(event.pubkey) - const { replies: olderReplies, until: newUntil } = await client.fetchMoreReplies( - relayList.read.slice(0, 5), - event.id, - until, - 100 - ) + const events = await client.loadMoreTimeline(timelineKey, until, LIMIT) + const olderReplies = events.filter((evt) => isReplyNoteEvent(evt)).reverse() if (olderReplies.length > 0) { setReplies((pre) => [...olderReplies, ...pre]) } - setUntil(newUntil) + setUntil(events.length ? events[events.length - 1].created_at - 1 : undefined) setLoading(false) } diff --git a/src/renderer/src/pages/secondary/NoteListPage/index.tsx b/src/renderer/src/pages/secondary/NoteListPage/index.tsx index 39cedbe..8f83db9 100644 --- a/src/renderer/src/pages/secondary/NoteListPage/index.tsx +++ b/src/renderer/src/pages/secondary/NoteListPage/index.tsx @@ -14,26 +14,26 @@ export default function NoteListPage() { const { title = '', filter, - specificRelayUrl + urls } = useMemo<{ title?: string filter?: Filter - specificRelayUrl?: string + urls: string[] }>(() => { const hashtag = searchParams.get('t') if (hashtag) { - return { title: `# ${hashtag}`, filter: { '#t': [hashtag] } } + return { title: `# ${hashtag}`, filter: { '#t': [hashtag] }, urls: relayUrls } } const search = searchParams.get('s') if (search) { - return { title: `${t('search')}: ${search}`, filter: { search } } + return { title: `${t('search')}: ${search}`, filter: { search }, urls: relayUrls } } const relayUrl = searchParams.get('relay') if (relayUrl && isWebsocketUrl(relayUrl)) { - return { title: relayUrl, specificRelayUrl: relayUrl } + return { title: relayUrl, urls: [relayUrl] } } - return {} - }, [searchParams]) + return { urls: relayUrls } + }, [searchParams, JSON.stringify(relayUrls)]) if (filter?.search && searchableRelayUrls.length === 0) { return ( @@ -47,11 +47,7 @@ export default function NoteListPage() { return ( - + ) } diff --git a/src/renderer/src/providers/NostrProvider/index.tsx b/src/renderer/src/providers/NostrProvider/index.tsx index 6796b8e..21fa025 100644 --- a/src/renderer/src/providers/NostrProvider/index.tsx +++ b/src/renderer/src/providers/NostrProvider/index.tsx @@ -160,7 +160,6 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { } setPubkey(null) await storage.setAccountInfo(null) - client.clearNotificationsCache() } const signEvent = async (draftEvent: TDraftEvent) => { diff --git a/src/renderer/src/services/client.service.ts b/src/renderer/src/services/client.service.ts index 11f4702..3b0f9d6 100644 --- a/src/renderer/src/services/client.service.ts +++ b/src/renderer/src/services/client.service.ts @@ -1,5 +1,4 @@ import { TDraftEvent } from '@common/types' -import { isReplyNoteEvent } from '@renderer/lib/event' import { formatPubkey } from '@renderer/lib/pubkey' import { tagNameEquals } from '@renderer/lib/tag' import { isWebsocketUrl, normalizeUrl } from '@renderer/lib/url' @@ -23,12 +22,23 @@ const BIG_RELAY_URLS = [ 'wss://relay.noswhere.com/' ] +type TTimelineRef = [string, number] + class ClientService { static instance: ClientService private defaultRelayUrls: string[] = BIG_RELAY_URLS private pool = new SimplePool() + private timelines: Record< + string, + | { + refs: TTimelineRef[] + filter: Omit & { limit: number } + urls: string[] + } + | undefined + > = {} private eventCache = new LRUCache>({ max: 10000 }) private eventDataLoader = new DataLoader( (ids) => Promise.all(ids.map((id) => this._fetchEvent(id))), @@ -38,10 +48,6 @@ class ClientService { this.eventBatchLoadFn.bind(this), { cache: false } ) - private repliesCache = new LRUCache({ - max: 1000 - }) - private notificationsCache: [string, number][] = [] private profileCache = new LRUCache>({ max: 10000 }) private profileDataloader = new DataLoader( (ids) => Promise.all(ids.map((id) => this._fetchProfile(id))), @@ -91,26 +97,65 @@ class ClientService { this.defaultRelayUrls = Array.from(new Set(urls.concat(BIG_RELAY_URLS))) } + getDefaultRelayUrls() { + return this.defaultRelayUrls + } + async publishEvent(relayUrls: string[], event: NEvent) { return await Promise.any(this.pool.publish(relayUrls, event)) } - subscribeEventsWithAuth( + private async generateTimelineKey(urls: string[], filter: Filter): Promise { + const paramsStr = JSON.stringify({ urls: urls.sort(), filter }) + const encoder = new TextEncoder() + const data = encoder.encode(paramsStr) + const hashBuffer = await crypto.subtle.digest('SHA-256', data) + const hashArray = Array.from(new Uint8Array(hashBuffer)) + return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('') + } + + async subscribeTimeline( urls: string[], - filter: Filter, + filter: Omit & { limit: number }, // filter with limit, { - onEose, + onEvents, onNew }: { - onEose: (events: NEvent[]) => void + onEvents: (events: NEvent[], eosed: boolean) => void onNew: (evt: NEvent) => void }, - signer?: (evt: TDraftEvent) => Promise + { + signer, + needSort = true + }: { + signer?: (evt: TDraftEvent) => Promise + needSort?: boolean + } = {} ) { + const key = await this.generateTimelineKey(urls, filter) + const timeline = this.timelines[key] + let cachedEvents: NEvent[] = [] + let since: number | undefined + if (timeline && timeline.refs.length) { + cachedEvents = ( + await Promise.all( + timeline.refs.slice(0, filter.limit).map(([id]) => this.eventCache.get(id)) + ) + ).filter(Boolean) as NEvent[] + if (cachedEvents.length) { + onEvents(cachedEvents, false) + since = cachedEvents[0].created_at + 1 + } + } + + if (!timeline && needSort) { + this.timelines[key] = { refs: [], filter, urls } + } + // eslint-disable-next-line @typescript-eslint/no-this-alias const that = this const _knownIds = new Set() - const events: NEvent[] = [] + let events: NEvent[] = [] let started = 0 let eosed = 0 const subPromises = urls.map(async (url) => { @@ -121,19 +166,52 @@ class ClientService { function startSub() { started++ - return relay.subscribe([filter], { + return relay.subscribe([since ? { ...filter, since } : filter], { alreadyHaveEvent: (id: string) => { const have = _knownIds.has(id) + if (have) { + return true + } _knownIds.add(id) - return have + return false }, onevent(evt: NEvent) { - if (eosed === started) { + that.eventDataLoader.prime(evt.id, Promise.resolve(evt)) + // not eosed yet, push to events + if (eosed < started) { + return events.push(evt) + } + // eosed, (algo relay feeds) no need to sort and cache + if (!needSort) { + return onNew(evt) + } + + const timeline = that.timelines[key] + if (!timeline || !timeline.refs.length) { + return onNew(evt) + } + // the event is newer than the first ref, insert it to the front + if (evt.created_at > timeline.refs[0][1]) { onNew(evt) - } else { - events.push(evt) + return timeline.refs.unshift([evt.id, evt.created_at]) } - that.eventDataLoader.prime(evt.id, Promise.resolve(evt)) + + let idx = 0 + for (const ref of timeline.refs) { + if (evt.created_at > ref[1] || (evt.created_at === ref[1] && evt.id < ref[0])) { + break + } + // the event is already in the cache + if (evt.created_at === ref[1] && evt.id === ref[0]) { + return + } + idx++ + } + // the event is too old, ignore it + if (idx >= timeline.refs.length) return + + // insert the event to the right position + timeline.refs.splice(idx, 0, [evt.id, evt.created_at]) }, onclose(reason: string) { if (reason.startsWith('auth-required:')) { @@ -151,255 +229,91 @@ class ClientService { }, oneose() { eosed++ - if (eosed === started) { - onEose(events) + if (eosed < started) return + + // (algo feeds) no need to sort and cache + if (!needSort) { + return onEvents(events, true) + } + events = events.sort((a, b) => b.created_at - a.created_at).slice(0, filter.limit) + + const timeline = that.timelines[key] + // no cache yet + if (!timeline || !timeline.refs.length) { + that.timelines[key] = { + refs: events.map((evt) => [evt.id, evt.created_at]), + filter, + urls + } + return onEvents(events, true) + } + + const newEvents = events.filter((evt) => { + const firstRef = timeline.refs[0] + return ( + evt.created_at > firstRef[1] || + (evt.created_at === firstRef[1] && evt.id < firstRef[0]) + ) + }) + const newRefs = newEvents.map((evt) => [evt.id, evt.created_at] as TTimelineRef) + + if (newRefs.length >= filter.limit) { + // if new refs are more than limit, means old refs are too old, replace them + timeline.refs = newRefs + onEvents(newEvents, true) + } else { + // merge new refs with old refs + timeline.refs = newRefs.concat(timeline.refs) + onEvents(newEvents.concat(cachedEvents), true) } } }) } }) - return () => { - onEose = () => {} - onNew = () => {} - subPromises.forEach((subPromise) => { - subPromise.then((sub) => { - sub.close() + return { + timelineKey: key, + closer: () => { + onEvents = () => {} + onNew = () => {} + subPromises.forEach((subPromise) => { + subPromise.then((sub) => { + sub.close() + }) }) - }) - } - } - - async subscribeReplies( - relayUrls: string[], - parentEventId: string, - limit: number, - { - onReplies, - onNew - }: { - onReplies: (events: NEvent[], isCache: boolean, until?: number) => void - onNew: (evt: NEvent) => void - } - ) { - let cache = this.repliesCache.get(parentEventId) - const refs = cache?.refs ?? [] - let replies: NEvent[] = [] - if (cache) { - replies = (await Promise.all(cache.refs.map(([id]) => this.eventCache.get(id)))).filter( - Boolean - ) as NEvent[] - onReplies(replies, true, cache.until) - } else { - cache = { refs } - this.repliesCache.set(parentEventId, cache) - } - const since = replies.length ? replies[replies.length - 1].created_at + 1 : undefined - - // eslint-disable-next-line @typescript-eslint/no-this-alias - const that = this - const events: NEvent[] = [] - let hasEosed = false - const closer = this.pool.subscribeMany( - relayUrls.length > 0 ? relayUrls : this.defaultRelayUrls, - [ - { - '#e': [parentEventId], - kinds: [kinds.ShortTextNote], - limit, - since - } - ], - { - onevent(evt: NEvent) { - if (hasEosed) { - if (!isReplyNoteEvent(evt)) return - onNew(evt) - } else { - events.push(evt) - } - that.eventDataLoader.prime(evt.id, Promise.resolve(evt)) - }, - oneose() { - hasEosed = true - const newReplies = events - .sort((a, b) => a.created_at - b.created_at) - .slice(0, limit) - .filter(isReplyNoteEvent) - replies = replies.concat(newReplies) - // first fetch - if (!since) { - cache.until = events.length >= limit ? events[0].created_at - 1 : undefined - } - onReplies(replies, false, cache.until) - const lastRefCreatedAt = refs.length ? refs[refs.length - 1][1] : undefined - if (lastRefCreatedAt) { - refs.push( - ...newReplies - .filter((reply) => reply.created_at > lastRefCreatedAt) - .map((evt) => [evt.id, evt.created_at] as [string, number]) - ) - } else { - refs.push(...newReplies.map((evt) => [evt.id, evt.created_at] as [string, number])) - } - } } - ) - - return () => { - onReplies = () => {} - onNew = () => {} - closer.close() } } - async subscribeNotifications( - pubkey: string, - limit: number, - { - onNotifications, - onNew - }: { - onNotifications: (events: NEvent[], isCache: boolean) => void - onNew: (evt: NEvent) => void - } - ) { - let cachedNotifications: NEvent[] = [] - if (this.notificationsCache.length) { - cachedNotifications = ( - await Promise.all(this.notificationsCache.map(([id]) => this.eventCache.get(id))) - ).filter(Boolean) as NEvent[] - onNotifications(cachedNotifications, true) - } - const since = this.notificationsCache.length ? this.notificationsCache[0][1] + 1 : undefined - - const relayList = await this.fetchRelayList(pubkey) - - // eslint-disable-next-line @typescript-eslint/no-this-alias - const that = this - const events: NEvent[] = [] - let hasEosed = false - let count = 0 - const closer = this.pool.subscribeMany( - relayList.read.length >= 4 - ? relayList.read - : relayList.read.concat(this.defaultRelayUrls).slice(0, 4), - [ - { - kinds: [kinds.ShortTextNote, kinds.Repost, kinds.Reaction], - '#p': [pubkey], - limit, - since - } - ], - { - onevent(evt: NEvent) { - count++ - if (hasEosed) { - if (evt.pubkey === pubkey) return - onNew(evt) - } else { - events.push(evt) - } - that.eventDataLoader.prime(evt.id, Promise.resolve(evt)) - }, - oneose() { - hasEosed = true - const newNotifications = events - .sort((a, b) => b.created_at - a.created_at) - .slice(0, limit) - .filter((evt) => evt.pubkey !== pubkey) - if (count >= limit) { - that.notificationsCache = newNotifications.map( - (evt) => [evt.id, evt.created_at] as [string, number] + async loadMoreTimeline(key: string, until: number, limit: number) { + const timeline = this.timelines[key] + if (!timeline) return [] + + const { filter, urls, refs } = timeline + const startIdx = refs.findIndex(([, createdAt]) => createdAt < until) + const cachedEvents = + startIdx >= 0 + ? (( + await Promise.all( + refs.slice(startIdx, startIdx + limit).map(([id]) => this.eventCache.get(id)) ) - onNotifications(newNotifications, false) - } else { - that.notificationsCache = [ - ...newNotifications.map((evt) => [evt.id, evt.created_at] as [string, number]), - ...that.notificationsCache - ] - onNotifications(newNotifications.concat(cachedNotifications), false) - } - } - } - ) - - return () => { - onNotifications = () => {} - onNew = () => {} - closer.close() + ).filter(Boolean) as NEvent[]) + : [] + if (cachedEvents.length >= limit) { + return cachedEvents } - } + const restLimit = limit - cachedEvents.length + const restUntil = cachedEvents.length + ? cachedEvents[cachedEvents.length - 1].created_at - 1 + : until - async fetchMoreReplies(relayUrls: string[], parentEventId: string, until: number, limit: number) { - let events = await this.pool.querySync(relayUrls, { - '#e': [parentEventId], - kinds: [kinds.ShortTextNote], - limit, - until - }) + let events = await this.pool.querySync(urls, { ...filter, until: restUntil, limit: restLimit }) events.forEach((evt) => { this.eventDataLoader.prime(evt.id, Promise.resolve(evt)) }) - events = events.sort((a, b) => a.created_at - b.created_at).slice(0, limit) - const replies = events.filter((evt) => isReplyNoteEvent(evt)) - let cache = this.repliesCache.get(parentEventId) - if (!cache) { - cache = { refs: [] } - this.repliesCache.set(parentEventId, cache) - } - const refs = cache.refs - const firstRefCreatedAt = refs.length ? refs[0][1] : undefined - const newRefs = firstRefCreatedAt - ? replies - .filter((evt) => evt.created_at < firstRefCreatedAt) - .map((evt) => [evt.id, evt.created_at] as [string, number]) - : replies.map((evt) => [evt.id, evt.created_at] as [string, number]) - - if (newRefs.length) { - refs.unshift(...newRefs) - } - cache.until = events.length >= limit ? events[0].created_at - 1 : undefined - return { replies, until: cache.until } - } - - async fetchMoreNotifications(pubkey: string, until: number, limit: number) { - const relayList = await this.fetchRelayList(pubkey) - const events = await this.pool.querySync( - relayList.read.length >= 4 - ? relayList.read - : relayList.read.concat(this.defaultRelayUrls).slice(0, 4), - { - kinds: [kinds.ShortTextNote, kinds.Repost, kinds.Reaction], - '#p': [pubkey], - limit, - until - } - ) - events.forEach((evt) => { - this.eventDataLoader.prime(evt.id, Promise.resolve(evt)) - }) - const notifications = events - .sort((a, b) => b.created_at - a.created_at) - .slice(0, limit) - .filter((evt) => evt.pubkey !== pubkey) - - const cacheLastCreatedAt = this.notificationsCache.length - ? this.notificationsCache[this.notificationsCache.length - 1][1] - : undefined - this.notificationsCache = this.notificationsCache.concat( - (cacheLastCreatedAt - ? notifications.filter((evt) => evt.created_at < cacheLastCreatedAt) - : notifications - ).map((evt) => [evt.id, evt.created_at] as [string, number]) - ) - - return notifications - } - - clearNotificationsCache() { - this.notificationsCache = [] + events = events.sort((a, b) => b.created_at - a.created_at).slice(0, restLimit) + timeline.refs.push(...events.map((evt) => [evt.id, evt.created_at] as TTimelineRef)) + return cachedEvents.concat(events) } async fetchEvents(relayUrls: string[], filter: Filter, cache = false) {