From 80fa5a132b8bd9b44ffd9e8803dcc9ca04d0c2ad Mon Sep 17 00:00:00 2001 From: Kieran Date: Tue, 9 Jan 2024 16:40:31 +0000 Subject: [PATCH] refactor: reactions grouping and other fixes --- .../app/src/Cache/EventInteractionCache.ts | 1 - packages/app/src/Cache/FollowsFeed.ts | 6 +- .../app/src/Components/Event/LongFormText.tsx | 9 +- .../src/Components/Event/NostrFileHeader.tsx | 4 +- .../src/Components/Event/Note/NoteFooter.tsx | 4 +- .../src/Components/Event/Note/NoteQuote.tsx | 4 +- .../Components/Event/Note/ReactionsModal.tsx | 6 +- packages/app/src/Components/Feed/Articles.tsx | 2 +- packages/app/src/Components/Feed/Generic.tsx | 7 +- .../src/Components/Feed/TimelineFollows.tsx | 2 +- .../src/Components/SearchBox/SearchBox.tsx | 2 +- .../app/src/Components/SendSats/SendSats.tsx | 2 - .../src/Components/Trending/TrendingPosts.tsx | 2 +- .../app/src/Components/User/ProfileLink.tsx | 3 +- packages/app/src/Feed/BadgesFeed.ts | 11 +- packages/app/src/Feed/FollowersFeed.ts | 2 +- packages/app/src/Feed/FollowsFeed.ts | 2 +- packages/app/src/Feed/LoginFeed.ts | 26 +-- packages/app/src/Feed/RelaysFeed.tsx | 2 +- packages/app/src/Feed/StatusFeed.ts | 2 +- packages/app/src/Feed/ThreadFeed.ts | 12 +- packages/app/src/Feed/TimelineFeed.ts | 41 ++-- packages/app/src/Feed/ZapsFeed.ts | 4 +- .../app/src/Hooks/useCommunityLeaders.tsx | 1 - packages/app/src/Hooks/useLists.tsx | 12 +- packages/app/src/Hooks/useRates.tsx | 11 +- .../app/src/Hooks/useRefreshFeedcache.tsx | 23 +-- packages/app/src/Pages/HashTagsPage.tsx | 6 +- packages/app/src/Pages/Layout/Header.tsx | 6 +- .../src/Pages/Messages/Nip28ChatProfile.tsx | 4 +- .../src/Pages/Notifications/Notifications.tsx | 2 +- .../app/src/Pages/settings/Preferences.tsx | 1 - .../app/src/Pages/settings/wallet/Alby.tsx | 1 - .../app/src/Utils/Login/MultiAccountStore.ts | 1 - packages/app/src/Utils/Notifications.ts | 3 +- packages/app/src/Utils/ZapPoolController.ts | 4 +- packages/app/src/Utils/index.ts | 1 - packages/shared/src/feed-cache.ts | 30 ++- packages/shared/src/lnurl.ts | 1 - packages/system-react/README.md | 6 +- packages/system-react/example/example.tsx | 8 +- packages/system-react/src/useEventFeed.ts | 2 +- packages/system-react/src/useReactions.ts | 2 +- .../system-react/src/useRequestBuilder.tsx | 69 ++++--- packages/system-svelte/src/request-builder.ts | 18 +- packages/system/README.md | 25 +-- packages/system/examples/simple.ts | 18 +- packages/system/src/background-loader.ts | 13 +- packages/system/src/cache/index.ts | 1 + ...-connection-pool.ts => connection-pool.ts} | 17 +- packages/system/src/connection.ts | 11 +- packages/system/src/index.ts | 20 +- packages/system/src/nostr-system.ts | 40 +--- packages/system/src/note-collection.ts | 178 ++---------------- packages/system/src/profile-cache.ts | 11 +- ...ostr-query-manager.ts => query-manager.ts} | 40 ++-- packages/system/src/query.ts | 101 +++++----- packages/system/src/worker/system-worker.ts | 2 +- 58 files changed, 344 insertions(+), 501 deletions(-) rename packages/system/src/{nostr-connection-pool.ts => connection-pool.ts} (91%) rename packages/system/src/{nostr-query-manager.ts => query-manager.ts} (78%) diff --git a/packages/app/src/Cache/EventInteractionCache.ts b/packages/app/src/Cache/EventInteractionCache.ts index c405fc19..9fe2c507 100644 --- a/packages/app/src/Cache/EventInteractionCache.ts +++ b/packages/app/src/Cache/EventInteractionCache.ts @@ -30,7 +30,6 @@ export class EventInteractionCache extends FeedCache { }); await this.bulkSet(toImport); - console.debug(`Imported dumb-zap-cache events: `, toImport.length); window.localStorage.removeItem("zap-cache"); } await this.buffer([...this.onTable]); diff --git a/packages/app/src/Cache/FollowsFeed.ts b/packages/app/src/Cache/FollowsFeed.ts index 09ef6e4b..1894b4bb 100644 --- a/packages/app/src/Cache/FollowsFeed.ts +++ b/packages/app/src/Cache/FollowsFeed.ts @@ -1,6 +1,5 @@ import { unixNow, unixNowMs } from "@snort/shared"; import { EventKind, RequestBuilder, SystemInterface, TaggedNostrEvent } from "@snort/system"; -import debug from "debug"; import { db } from "@/Db"; import { Day, Hour } from "@/Utils/Const"; @@ -68,8 +67,7 @@ export class FollowsFeedCache extends RefreshFeedCache { const oldest = await this.table?.orderBy("created_at").first(); this.#oldest = oldest?.created_at; this.emit("change", latest?.map(a => this.key(a)) ?? []); - - debug(this.name)(`Loaded %d/%d in %d ms`, latest?.length ?? 0, keys.length, (unixNowMs() - start).toLocaleString()); + this.log(`Loaded %d/%d in %d ms`, latest?.length ?? 0, keys.length, (unixNowMs() - start).toLocaleString()); } async loadMore(system: SystemInterface, session: LoginSession, before: number) { @@ -132,7 +130,7 @@ export class FollowsFeedCache extends RefreshFeedCache { const allKeys = new Set(everything?.map(a => a.pubkey)); const missingKeys = keys.filter(a => !allKeys.has(a)); await this.backFill(system, missingKeys); - debug(this.name)(`Backfilled %d keys in %d ms`, missingKeys.length, (unixNowMs() - start).toLocaleString()); + this.log(`Backfilled %d keys in %d ms`, missingKeys.length, (unixNowMs() - start).toLocaleString()); } } } diff --git a/packages/app/src/Components/Event/LongFormText.tsx b/packages/app/src/Components/Event/LongFormText.tsx index 988e31a8..afc7858a 100644 --- a/packages/app/src/Components/Event/LongFormText.tsx +++ b/packages/app/src/Components/Event/LongFormText.tsx @@ -32,13 +32,8 @@ export function LongFormText(props: LongFormTextProps) { const [reading, setReading] = useState(false); const [showMore, setShowMore] = useState(false); const ref = useRef(null); - const related = useReactions( - NostrLink.fromEvent(props.ev).id + "related", - [NostrLink.fromEvent(props.ev)], - undefined, - false, - ); - const { reactions, reposts, zaps } = useEventReactions(NostrLink.fromEvent(props.ev), related.data ?? []); + const related = useReactions("note:reactions", [NostrLink.fromEvent(props.ev)], undefined, false); + const { reactions, reposts, zaps } = useEventReactions(NostrLink.fromEvent(props.ev), related); function previewText() { return ( diff --git a/packages/app/src/Components/Event/NostrFileHeader.tsx b/packages/app/src/Components/Event/NostrFileHeader.tsx index 243ea698..6aa6946e 100644 --- a/packages/app/src/Components/Event/NostrFileHeader.tsx +++ b/packages/app/src/Components/Event/NostrFileHeader.tsx @@ -10,8 +10,8 @@ import { findTag } from "@/Utils"; export default function NostrFileHeader({ link }: { link: NostrLink }) { const ev = useEventFeed(link); - if (!ev.data) return ; - return ; + if (!ev) return ; + return ; } export function NostrFileElement({ ev }: { ev: NostrEvent }) { diff --git a/packages/app/src/Components/Event/Note/NoteFooter.tsx b/packages/app/src/Components/Event/Note/NoteFooter.tsx index 7af4b749..1b9dedce 100644 --- a/packages/app/src/Components/Event/Note/NoteFooter.tsx +++ b/packages/app/src/Components/Event/Note/NoteFooter.tsx @@ -36,8 +36,8 @@ export default function NoteFooter(props: NoteFooterProps) { const link = useMemo(() => NostrLink.fromEvent(ev), [ev.id]); const ids = useMemo(() => [link], [link]); - const related = useReactions(link.id + "related", ids, undefined, false); - const { reactions, zaps, reposts } = useEventReactions(link, related.data ?? []); + const related = useReactions("note:reactions", ids, undefined, false); + const { reactions, zaps, reposts } = useEventReactions(link, related); const { positive } = reactions; const { formatMessage } = useIntl(); diff --git a/packages/app/src/Components/Event/Note/NoteQuote.tsx b/packages/app/src/Components/Event/Note/NoteQuote.tsx index 49f3838f..f99da674 100644 --- a/packages/app/src/Components/Event/Note/NoteQuote.tsx +++ b/packages/app/src/Components/Event/Note/NoteQuote.tsx @@ -11,11 +11,11 @@ const options = { export default function NoteQuote({ link, depth }: { link: NostrLink; depth?: number }) { const ev = useEventFeed(link); - if (!ev.data) + if (!ev) return (
); - return ; + return ; } diff --git a/packages/app/src/Components/Event/Note/ReactionsModal.tsx b/packages/app/src/Components/Event/Note/ReactionsModal.tsx index 5a1f6c84..26451e74 100644 --- a/packages/app/src/Components/Event/Note/ReactionsModal.tsx +++ b/packages/app/src/Components/Event/Note/ReactionsModal.tsx @@ -26,11 +26,11 @@ const ReactionsModal = ({ show, setShow, event }: ReactionsModalProps) => { const link = NostrLink.fromEvent(event); - const related = useReactions(link.id + "related", [link], undefined, false); - const { reactions, zaps, reposts } = useEventReactions(link, related.data ?? []); + const related = useReactions("note:reactions", [link], undefined, false); + const { reactions, zaps, reposts } = useEventReactions(link, related); const { positive, negative } = reactions; - const sortEvents = events => + const sortEvents = (events: Array) => events.sort( (a, b) => socialGraphInstance.getFollowDistance(a.pubkey) - socialGraphInstance.getFollowDistance(b.pubkey), ); diff --git a/packages/app/src/Components/Feed/Articles.tsx b/packages/app/src/Components/Feed/Articles.tsx index d93fff60..8d63311f 100644 --- a/packages/app/src/Components/Feed/Articles.tsx +++ b/packages/app/src/Components/Feed/Articles.tsx @@ -16,7 +16,7 @@ export default function Articles() { return ( <> - {orderDescending(data.data ?? []).map(a => ( + {orderDescending(data).map(a => ( { - console.debug(link); const sub = new RequestBuilder("generic"); sub.withOptions({ leaveOpen: true }); const reqs = JSON.parse(link.id) as Array; @@ -17,11 +16,11 @@ export function GenericFeed({ link }: { link: NostrLink }) { return sub; }, [link]); - const evs = useRequestBuilder(NoteCollection, sub); + const evs = useRequestBuilder(sub); return ( { //nothing diff --git a/packages/app/src/Components/Feed/TimelineFollows.tsx b/packages/app/src/Components/Feed/TimelineFollows.tsx index e7056fbb..d4f9132d 100644 --- a/packages/app/src/Components/Feed/TimelineFollows.tsx +++ b/packages/app/src/Components/Feed/TimelineFollows.tsx @@ -70,7 +70,7 @@ const TimelineFollows = (props: TimelineFollowsProps) => { }; const mixinFiltered = useMemo(() => { const mainFeedIds = new Set(mainFeed.map(a => a.id)); - return (mixin.data.data ?? []) + return (mixin.data ?? []) .filter(a => !mainFeedIds.has(a.id) && postsOnly(a) && !isEventMuted(a)) .filter(a => a.tags.filter(a => a[0] === "t").length < 5) .filter(a => !oldest || a.created_at >= oldest) diff --git a/packages/app/src/Components/SearchBox/SearchBox.tsx b/packages/app/src/Components/SearchBox/SearchBox.tsx index 54ea8d2f..17122378 100644 --- a/packages/app/src/Components/SearchBox/SearchBox.tsx +++ b/packages/app/src/Components/SearchBox/SearchBox.tsx @@ -38,7 +38,7 @@ export default function SearchBox() { const subject: TimelineSubject = { type: "profile_keyword", discriminator: search, - items: [search], + items: search ? [search] : [], relay: undefined, streams: false, }; diff --git a/packages/app/src/Components/SendSats/SendSats.tsx b/packages/app/src/Components/SendSats/SendSats.tsx index 92d7a3e8..182c6b99 100644 --- a/packages/app/src/Components/SendSats/SendSats.tsx +++ b/packages/app/src/Components/SendSats/SendSats.tsx @@ -84,10 +84,8 @@ export default function SendSats(props: SendSatsProps) { useEffect(() => { if (props.targets && props.show) { try { - console.debug("loading zapper"); const zapper = new Zapper(system, publisher); zapper.load(props.targets).then(() => { - console.debug(zapper); setZapper(zapper); }); } catch (e) { diff --git a/packages/app/src/Components/Trending/TrendingPosts.tsx b/packages/app/src/Components/Trending/TrendingPosts.tsx index 6c0756ad..c64e319e 100644 --- a/packages/app/src/Components/Trending/TrendingPosts.tsx +++ b/packages/app/src/Components/Trending/TrendingPosts.tsx @@ -31,7 +31,7 @@ export default function TrendingNotes({ count = Infinity, small = false }: { cou return removeUndefined( data.notes.map(a => { const ev = a.event; - if (!System.Optimizer.schnorrVerify(ev)) { + if (!System.optimizer.schnorrVerify(ev)) { console.error(`Event with invalid sig\n\n${ev}\n\nfrom ${trendingNotesUrl}`); return; } diff --git a/packages/app/src/Components/User/ProfileLink.tsx b/packages/app/src/Components/User/ProfileLink.tsx index b70a9f60..510bf9e5 100644 --- a/packages/app/src/Components/User/ProfileLink.tsx +++ b/packages/app/src/Components/User/ProfileLink.tsx @@ -18,7 +18,8 @@ export function ProfileLink({ children?: ReactNode; } & Omit) { const system = useContext(SnortContext); - const relays = system.relayCache.getFromCache(pubkey) + const relays = system.relayCache + .getFromCache(pubkey) ?.relays?.filter(a => a.settings.write) ?.map(a => a.url); diff --git a/packages/app/src/Feed/BadgesFeed.ts b/packages/app/src/Feed/BadgesFeed.ts index 630e83d2..3f7d31f9 100644 --- a/packages/app/src/Feed/BadgesFeed.ts +++ b/packages/app/src/Feed/BadgesFeed.ts @@ -20,11 +20,8 @@ export default function useProfileBadges(pubkey?: HexKey) { const profileBadges = useRequestBuilder(sub); const profile = useMemo(() => { - if (profileBadges.data) { - return chunks( - profileBadges.data[0].tags.filter(t => t[0] === "a" || t[0] === "e"), - 2, - ).reduce((acc, [a, e]) => { + if (profileBadges) { + return chunks(profileBadges[0]?.tags.filter(t => t[0] === "a" || t[0] === "e"), 2).reduce((acc, [a, e]) => { return { ...acc, [e[1]]: a[1], @@ -60,8 +57,8 @@ export default function useProfileBadges(pubkey?: HexKey) { const awards = useRequestBuilder(awardsSub); const result = useMemo(() => { - if (awards.data) { - return awards.data + if (awards) { + return awards .map((award, _, arr) => { const [, pubkey, d] = award.tags diff --git a/packages/app/src/Feed/FollowersFeed.ts b/packages/app/src/Feed/FollowersFeed.ts index 6ebd6e99..fdd76823 100644 --- a/packages/app/src/Feed/FollowersFeed.ts +++ b/packages/app/src/Feed/FollowersFeed.ts @@ -13,7 +13,7 @@ export default function useFollowersFeed(pubkey?: HexKey) { const followersFeed = useRequestBuilder(sub); const followers = useMemo(() => { - const contactLists = followersFeed.data?.filter( + const contactLists = followersFeed?.filter( a => a.kind === EventKind.ContactList && a.tags.some(b => b[0] === "p" && b[1] === pubkey), ); return [...new Set(contactLists?.map(a => a.pubkey))].sort((a, b) => { diff --git a/packages/app/src/Feed/FollowsFeed.ts b/packages/app/src/Feed/FollowsFeed.ts index d5efa388..fb5157ea 100644 --- a/packages/app/src/Feed/FollowsFeed.ts +++ b/packages/app/src/Feed/FollowsFeed.ts @@ -21,7 +21,7 @@ export default function useFollowsFeed(pubkey?: HexKey) { return follows.item; } - return getFollowing(contactFeed.data ?? [], pubkey); + return getFollowing(contactFeed ?? [], pubkey); }, [contactFeed, follows, pubkey]); } diff --git a/packages/app/src/Feed/LoginFeed.ts b/packages/app/src/Feed/LoginFeed.ts index d848d952..9c5e7e8c 100644 --- a/packages/app/src/Feed/LoginFeed.ts +++ b/packages/app/src/Feed/LoginFeed.ts @@ -100,8 +100,8 @@ export default function useLoginFeed() { // update relays and follow lists useEffect(() => { - if (loginFeed.data) { - const contactList = getNewest(loginFeed.data.filter(a => a.kind === EventKind.ContactList)); + if (loginFeed) { + const contactList = getNewest(loginFeed.filter(a => a.kind === EventKind.ContactList)); if (contactList) { const pTags = contactList.tags.filter(a => a[0] === "p").map(a => a[1]); setFollows(login.id, pTags, contactList.created_at * 1000); @@ -109,17 +109,17 @@ export default function useLoginFeed() { FollowsFeed.backFillIfMissing(system, pTags); } - const relays = getNewest(loginFeed.data.filter(a => a.kind === EventKind.Relays)); + const relays = getNewest(loginFeed.filter(a => a.kind === EventKind.Relays)); if (relays) { const parsedRelays = parseRelayTags(relays.tags.filter(a => a[0] === "r")).map(a => [a.url, a.settings]); setRelays(login, Object.fromEntries(parsedRelays), relays.created_at * 1000); } - Nip4Chats.onEvent(loginFeed.data); - Nip28Chats.onEvent(loginFeed.data); + Nip4Chats.onEvent(loginFeed); + Nip28Chats.onEvent(loginFeed); if (publisher) { - const subs = loginFeed.data.filter( + const subs = loginFeed.filter( a => a.kind === EventKind.SnortSubscriptions && a.pubkey === bech32ToHex(SnortPubKey), ); Promise.all( @@ -135,7 +135,7 @@ export default function useLoginFeed() { }), ).then(a => addSubscription(login, ...a.filter(a => a !== undefined).map(unwrap))); - const appData = getNewest(loginFeed.data.filter(a => a.kind === EventKind.AppData)); + const appData = getNewest(loginFeed.filter(a => a.kind === EventKind.AppData)); if (appData) { publisher.decryptGeneric(appData.content, appData.pubkey).then(d => { setAppData(login, JSON.parse(d) as SnortAppData, appData.created_at * 1000); @@ -200,20 +200,20 @@ export default function useLoginFeed() { } useEffect(() => { - if (loginFeed.data) { - const mutedFeed = loginFeed.data.filter(a => a.kind === EventKind.MuteList); + if (loginFeed) { + const mutedFeed = loginFeed.filter(a => a.kind === EventKind.MuteList); handleMutedFeed(mutedFeed); - const pinnedFeed = loginFeed.data.filter(a => a.kind === EventKind.PinList); + const pinnedFeed = loginFeed.filter(a => a.kind === EventKind.PinList); handlePinnedFeed(pinnedFeed); - const tagsFeed = loginFeed.data.filter(a => a.kind === EventKind.InterestsList); + const tagsFeed = loginFeed.filter(a => a.kind === EventKind.InterestsList); handleTagFeed(tagsFeed); - const bookmarkFeed = loginFeed.data.filter(a => a.kind === EventKind.BookmarksList); + const bookmarkFeed = loginFeed.filter(a => a.kind === EventKind.BookmarksList); handleBookmarkFeed(bookmarkFeed); - const publicChatsFeed = loginFeed.data.filter(a => a.kind === EventKind.PublicChatsList); + const publicChatsFeed = loginFeed.filter(a => a.kind === EventKind.PublicChatsList); handlePublicChatsListFeed(publicChatsFeed); } }, [loginFeed]); diff --git a/packages/app/src/Feed/RelaysFeed.tsx b/packages/app/src/Feed/RelaysFeed.tsx index c2c717ed..8273935c 100644 --- a/packages/app/src/Feed/RelaysFeed.tsx +++ b/packages/app/src/Feed/RelaysFeed.tsx @@ -11,5 +11,5 @@ export default function useRelaysFeed(pubkey?: HexKey) { }, [pubkey]); const relays = useRequestBuilder(sub); - return parseRelayTags(relays.data?.[0].tags.filter(a => a[0] === "r") ?? []); + return parseRelayTags(relays[0]?.tags.filter(a => a[0] === "r") ?? []); } diff --git a/packages/app/src/Feed/StatusFeed.ts b/packages/app/src/Feed/StatusFeed.ts index 4cedfe53..91732b14 100644 --- a/packages/app/src/Feed/StatusFeed.ts +++ b/packages/app/src/Feed/StatusFeed.ts @@ -20,7 +20,7 @@ export function useStatusFeed(id?: string, leaveOpen = false) { const status = useRequestBuilder(sub); - const statusFiltered = status.data?.filter(a => { + const statusFiltered = status.filter(a => { const exp = Number(findTag(a, "expiration")); return isNaN(exp) || exp >= unixNow(); }); diff --git a/packages/app/src/Feed/ThreadFeed.ts b/packages/app/src/Feed/ThreadFeed.ts index aca4e372..f944bafb 100644 --- a/packages/app/src/Feed/ThreadFeed.ts +++ b/packages/app/src/Feed/ThreadFeed.ts @@ -33,8 +33,8 @@ export default function useThreadFeed(link: NostrLink) { const store = useRequestBuilder(sub); useEffect(() => { - if (store.data) { - const links = store.data + if (store) { + const links = store .map(a => [ NostrLink.fromEvent(a), ...a.tags.filter(a => a[0] === "e" || a[0] === "a").map(v => NostrLink.fromTag(v)), @@ -42,7 +42,7 @@ export default function useThreadFeed(link: NostrLink) { .flat(); setAllEvents(links); - const current = store.data.find(a => link.matchesEvent(a)); + const current = store.find(a => link.matchesEvent(a)); if (current) { const t = EventExt.extractThread(current); if (t) { @@ -60,12 +60,12 @@ export default function useThreadFeed(link: NostrLink) { } } } - }, [store.data?.length]); + }, [store?.length]); const reactions = useReactions(`thread:${link.id.slice(0, 12)}:reactions`, [link, ...allEvents]); return { - thread: store.data ?? [], - reactions: reactions.data ?? [], + thread: store ?? [], + reactions: reactions ?? [], }; } diff --git a/packages/app/src/Feed/TimelineFeed.ts b/packages/app/src/Feed/TimelineFeed.ts index fd38f44b..59096c0c 100644 --- a/packages/app/src/Feed/TimelineFeed.ts +++ b/packages/app/src/Feed/TimelineFeed.ts @@ -1,7 +1,7 @@ import { unixNow } from "@snort/shared"; import { EventKind, RequestBuilder } from "@snort/system"; -import { useRequestBuilder } from "@snort/system-react"; -import { useCallback, useMemo } from "react"; +import { useRequestBuilderAdvanced } from "@snort/system-react"; +import { useCallback, useMemo, useSyncExternalStore } from "react"; import useLogin from "@/Hooks/useLogin"; import useTimelineWindow from "@/Hooks/useTimelineWindow"; @@ -116,7 +116,16 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel return rb?.builder ?? null; }, [until, since, options.method, pref, createBuilder]); - const main = useRequestBuilder(sub); + const mainQuery = useRequestBuilderAdvanced(sub); + const main = useSyncExternalStore( + h => { + mainQuery?.on("event", h); + return () => { + mainQuery?.off("event", h); + }; + }, + () => mainQuery?.snapshot, + ); const subRealtime = useMemo(() => { const rb = createBuilder(); @@ -130,17 +139,25 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel return rb?.builder ?? null; }, [pref.autoShowLatest, createBuilder]); - const latest = useRequestBuilder(subRealtime); + const latestQuery = useRequestBuilderAdvanced(subRealtime); + const latest = useSyncExternalStore( + h => { + latestQuery?.on("event", h); + return () => { + latestQuery?.off("event", h); + }; + }, + () => latestQuery?.snapshot, + ); return { - main: main.data, - latest: latest.data, - loading: main.loading(), + main: main, + latest: latest, loadMore: () => { - if (main.data) { + if (main) { console.debug("Timeline load more!"); if (options.method === "LIMIT_UNTIL") { - const oldest = main.data.reduce((acc, v) => (acc = v.created_at < acc ? v.created_at : acc), unixNow()); + const oldest = main.reduce((acc, v) => (acc = v.created_at < acc ? v.created_at : acc), unixNow()); setUntil(oldest); } else { older(); @@ -148,9 +165,9 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel } }, showLatest: () => { - if (latest.data) { - main.add(latest.data); - latest.clear(); + if (latest) { + mainQuery?.feed.add(latest); + latestQuery?.feed.clear(); } }, }; diff --git a/packages/app/src/Feed/ZapsFeed.ts b/packages/app/src/Feed/ZapsFeed.ts index d6a5062e..480fd00c 100644 --- a/packages/app/src/Feed/ZapsFeed.ts +++ b/packages/app/src/Feed/ZapsFeed.ts @@ -13,8 +13,8 @@ export default function useZapsFeed(link?: NostrLink) { const zapsFeed = useRequestBuilder(sub); const zaps = useMemo(() => { - if (zapsFeed.data) { - const profileZaps = zapsFeed.data.map(a => parseZap(a)).filter(z => z.valid); + if (zapsFeed) { + const profileZaps = zapsFeed.map(a => parseZap(a)).filter(z => z.valid); profileZaps.sort((a, b) => b.amount - a.amount); return profileZaps; } diff --git a/packages/app/src/Hooks/useCommunityLeaders.tsx b/packages/app/src/Hooks/useCommunityLeaders.tsx index 6860de52..c84cea5c 100644 --- a/packages/app/src/Hooks/useCommunityLeaders.tsx +++ b/packages/app/src/Hooks/useCommunityLeaders.tsx @@ -27,7 +27,6 @@ export function useCommunityLeaders() { }); useEffect(() => { - console.debug("CommunityLeaders", list); LeadersStore.setLeaders(list.map(a => a.id)); }, [list]); } diff --git a/packages/app/src/Hooks/useLists.tsx b/packages/app/src/Hooks/useLists.tsx index 6e73cdd9..64f97547 100644 --- a/packages/app/src/Hooks/useLists.tsx +++ b/packages/app/src/Hooks/useLists.tsx @@ -1,4 +1,4 @@ -import { EventKind, NostrLink, NoteCollection, RequestBuilder } from "@snort/system"; +import { EventKind, NostrLink, RequestBuilder } from "@snort/system"; import { useEventsFeed, useRequestBuilder } from "@snort/system-react"; import { useMemo } from "react"; @@ -12,18 +12,18 @@ export function useLinkList(id: string, fn: (rb: RequestBuilder) => void) { return rb; }, [id, fn]); - const listStore = useRequestBuilder(NoteCollection, sub); + const listStore = useRequestBuilder(sub); return useMemo(() => { - if (listStore.data && listStore.data.length > 0) { - return listStore.data.map(e => NostrLink.fromTags(e.tags)).flat(); + if (listStore && listStore.length > 0) { + return listStore.map(e => NostrLink.fromTags(e.tags)).flat(); } return []; - }, [listStore.data]); + }, [listStore]); } export function useLinkListEvents(id: string, fn: (rb: RequestBuilder) => void) { const links = useLinkList(id, fn); - return useEventsFeed(`${id}:events`, links).data ?? []; + return useEventsFeed(`${id}:events`, links); } export function usePinList(pubkey: string | undefined) { diff --git a/packages/app/src/Hooks/useRates.tsx b/packages/app/src/Hooks/useRates.tsx index a8aa28e9..74701226 100644 --- a/packages/app/src/Hooks/useRates.tsx +++ b/packages/app/src/Hooks/useRates.tsx @@ -1,8 +1,10 @@ import { bech32ToHex } from "@snort/shared"; -import { EventKind, ReplaceableNoteStore, RequestBuilder } from "@snort/system"; +import { EventKind, RequestBuilder } from "@snort/system"; import { useRequestBuilder } from "@snort/system-react"; import { useMemo } from "react"; +import { getNewest } from "@/Utils"; + // Snort backend publishes rates const SnortPubkey = "npub1sn0rtcjcf543gj4wsg7fa59s700d5ztys5ctj0g69g2x6802npjqhjjtws"; @@ -20,12 +22,13 @@ export function useRates(symbol: string, leaveOpen = true) { return rb; }, [symbol]); - const data = useRequestBuilder(ReplaceableNoteStore, sub); + const feed = useRequestBuilder(sub); + const ev = getNewest(feed); - const tag = data?.data?.tags.find(a => a[0] === "d" && a[1] === symbol); + const tag = ev?.tags.find(a => a[0] === "d" && a[1] === symbol); if (!tag) return undefined; return { - time: data.data?.created_at, + time: ev?.created_at, ask: Number(tag[2]), bid: Number(tag[3]), low: Number(tag[4]), diff --git a/packages/app/src/Hooks/useRefreshFeedcache.tsx b/packages/app/src/Hooks/useRefreshFeedcache.tsx index 3f86a78c..f0f3b7e6 100644 --- a/packages/app/src/Hooks/useRefreshFeedcache.tsx +++ b/packages/app/src/Hooks/useRefreshFeedcache.tsx @@ -1,5 +1,5 @@ import { unwrap } from "@snort/shared"; -import { NoopStore, RequestBuilder, TaggedNostrEvent } from "@snort/system"; +import { RequestBuilder, TaggedNostrEvent } from "@snort/system"; import { useEffect, useMemo } from "react"; import { RefreshFeedCache } from "@/Cache/RefreshFeedCache"; @@ -25,23 +25,14 @@ export function useRefreshFeedCache(c: RefreshFeedCache, leaveOpen = false useEffect(() => { if (sub) { - const q = system.Query(NoopStore, sub); - let t: ReturnType | undefined; - let tBuf: Array = []; - q.on("event", evs => { - if (!t) { - tBuf = [...evs]; - t = setTimeout(() => { - t = undefined; - c.onEvent(tBuf, unwrap(login.publicKey), publisher); - }, 100); - } else { - tBuf.push(...evs); - } - }); + const q = system.Query(sub); + const handler = (evs: Array) => { + c.onEvent(evs, unwrap(login.publicKey), publisher); + }; + q.on("event", handler); q.uncancel(); return () => { - q.off("event"); + q.off("event", handler); q.cancel(); }; } diff --git a/packages/app/src/Pages/HashTagsPage.tsx b/packages/app/src/Pages/HashTagsPage.tsx index 4f782c4e..9985735c 100644 --- a/packages/app/src/Pages/HashTagsPage.tsx +++ b/packages/app/src/Pages/HashTagsPage.tsx @@ -1,5 +1,5 @@ import { dedupe } from "@snort/shared"; -import { EventKind, NoteCollection, RequestBuilder } from "@snort/system"; +import { EventKind, RequestBuilder } from "@snort/system"; import { useRequestBuilder } from "@snort/system-react"; import classNames from "classnames"; import { useMemo } from "react"; @@ -59,8 +59,8 @@ export function HashTagHeader({ tag, events, className }: { tag: string; events? rb.withFilter().kinds([EventKind.InterestsList]).tag("t", [tag.toLowerCase()]); return rb; }, [tag]); - const followsTag = useRequestBuilder(NoteCollection, sub); - const pubkeys = dedupe((followsTag.data ?? []).map(a => a.pubkey)); + const followsTag = useRequestBuilder(sub); + const pubkeys = dedupe(followsTag.map(a => a.pubkey)); return (
diff --git a/packages/app/src/Pages/Layout/Header.tsx b/packages/app/src/Pages/Layout/Header.tsx index 1e640562..e315c198 100644 --- a/packages/app/src/Pages/Layout/Header.tsx +++ b/packages/app/src/Pages/Layout/Header.tsx @@ -103,10 +103,10 @@ function NoteTitle({ link }: { link: NostrLink }) { const ev = useEventFeed(link); const values = useMemo(() => { - return { name: }; - }, [ev.data?.pubkey]); + return { name: }; + }, [ev?.pubkey]); - if (!ev.data?.pubkey) { + if (!ev?.pubkey) { return ; } diff --git a/packages/app/src/Pages/Messages/Nip28ChatProfile.tsx b/packages/app/src/Pages/Messages/Nip28ChatProfile.tsx index d8345d77..a9bc5d1b 100644 --- a/packages/app/src/Pages/Messages/Nip28ChatProfile.tsx +++ b/packages/app/src/Pages/Messages/Nip28ChatProfile.tsx @@ -6,8 +6,8 @@ import ProfilePreview from "@/Components/User/ProfilePreview"; export default function Nip28ChatProfile({ id, onClick }: { id: string; onClick: (id: string) => void }) { const channel = useEventFeed(new NostrLink(CONFIG.eventLinkPrefix, id, 40)); - if (channel?.data) { - const meta = JSON.parse(channel.data.content) as UserMetadata; + if (channel) { + const meta = JSON.parse(channel.content) as UserMetadata; return ( ; onC } function NotificationContext({ link, onClick }: { link: NostrLink; onClick: () => void }) { - const { data: ev } = useEventFeed(link); + const ev = useEventFeed(link); if (link.type === NostrPrefix.PublicKey) { return } />; } diff --git a/packages/app/src/Pages/settings/Preferences.tsx b/packages/app/src/Pages/settings/Preferences.tsx index 79f37b9b..ae876499 100644 --- a/packages/app/src/Pages/settings/Preferences.tsx +++ b/packages/app/src/Pages/settings/Preferences.tsx @@ -415,7 +415,6 @@ const PreferencesPage = () => { value={perf.reactionEmoji} onChange={e => { const split = e.target.value.match(/[\p{L}\S]{1}/u); - console.debug(e.target.value, split); updatePreferences(id, { ...perf, reactionEmoji: split?.[0] ?? "", diff --git a/packages/app/src/Pages/settings/wallet/Alby.tsx b/packages/app/src/Pages/settings/wallet/Alby.tsx index 8112bbee..f3e3056d 100644 --- a/packages/app/src/Pages/settings/wallet/Alby.tsx +++ b/packages/app/src/Pages/settings/wallet/Alby.tsx @@ -19,7 +19,6 @@ export default function AlbyOAuth() { async function setupWallet(token: string) { try { const auth = await alby.getToken(token); - console.debug(auth); const connection = new AlbyWallet(auth, () => {}); const info = await connection.getInfo(); diff --git a/packages/app/src/Utils/Login/MultiAccountStore.ts b/packages/app/src/Utils/Login/MultiAccountStore.ts index 8518ed0e..850a34d8 100644 --- a/packages/app/src/Utils/Login/MultiAccountStore.ts +++ b/packages/app/src/Utils/Login/MultiAccountStore.ts @@ -221,7 +221,6 @@ export class MultiAccountStore extends ExternalStore { updateSession(s: LoginSession) { if (this.#accounts.has(s.id)) { this.#accounts.set(s.id, s); - console.debug("SET SESSION", s); this.#save(); } } diff --git a/packages/app/src/Utils/Notifications.ts b/packages/app/src/Utils/Notifications.ts index e6340539..cca512b7 100644 --- a/packages/app/src/Utils/Notifications.ts +++ b/packages/app/src/Utils/Notifications.ts @@ -78,8 +78,7 @@ export async function subscribeToNotifications(publisher: EventPublisher) { if ("Notification" in window) { try { if (Notification.permission !== "granted") { - const res = await Notification.requestPermission(); - console.debug(res); + await Notification.requestPermission(); } } catch (e) { console.error(e); diff --git a/packages/app/src/Utils/ZapPoolController.ts b/packages/app/src/Utils/ZapPoolController.ts index 253baabc..ee9e94a8 100644 --- a/packages/app/src/Utils/ZapPoolController.ts +++ b/packages/app/src/Utils/ZapPoolController.ts @@ -1,4 +1,5 @@ import { ExternalStore, LNURL, unixNow } from "@snort/shared"; +import debug from "debug"; import { UserCache } from "@/Cache"; import { Toastore } from "@/Components/Toaster/Toaster"; @@ -21,6 +22,7 @@ export interface ZapPoolRecipient { } class ZapPool extends ExternalStore> { + #log = debug("ZapPool"); #store = new Map(); #isPayoutInProgress = false; #lastPayout = 0; @@ -50,7 +52,7 @@ class ZapPool extends ExternalStore> { const invoice = await svc.getInvoice(amtSend, `SnortZapPool: ${x.split}%`); if (invoice.pr) { const result = await wallet.payInvoice(invoice.pr); - console.debug("ZPC", invoice, result); + this.#log("%o %o", invoice, result); if (result.state === WalletInvoiceState.Paid) { x.sum -= amtSend; Toastore.push({ diff --git a/packages/app/src/Utils/index.ts b/packages/app/src/Utils/index.ts index fac3854d..4d7bae11 100644 --- a/packages/app/src/Utils/index.ts +++ b/packages/app/src/Utils/index.ts @@ -55,7 +55,6 @@ export async function openFile(): Promise { () => { setTimeout(() => { if (!lock) { - console.debug("FOCUS WINDOW UPLOAD"); resolve(undefined); } }, 300); diff --git a/packages/shared/src/feed-cache.ts b/packages/shared/src/feed-cache.ts index 764c73a3..ab8a4099 100644 --- a/packages/shared/src/feed-cache.ts +++ b/packages/shared/src/feed-cache.ts @@ -20,7 +20,7 @@ export interface FeedCacheEvents { export abstract class FeedCache extends EventEmitter { readonly name: string; #snapshot: Array = []; - #log: ReturnType; + protected log: ReturnType; #hits = 0; #miss = 0; protected table?: DexieTableLike; @@ -31,9 +31,9 @@ export abstract class FeedCache extends EventEmitter { super(); this.name = name; this.table = table; - this.#log = debug(name); + this.log = debug(name); setInterval(() => { - this.#log( + this.log( "%d loaded, %d on-disk, %d hooks, %d% hit", this.cache.size, this.onTable.size, @@ -55,21 +55,15 @@ export abstract class FeedCache extends EventEmitter { } hook(fn: HookFn, key: string | undefined) { - if (key) { - const handle = (keys: Array) => { - if (keys.includes(key)) { - fn(); - } - }; - this.on("change", handle); - return () => this.off("change", handle); - } - - return () => { - // noop + const handle = (keys: Array) => { + if (!key || keys.includes(key)) { + fn(); + } }; + this.on("change", handle); + return () => this.off("change", handle); } - + keysOnTable() { return [...this.onTable]; } @@ -161,7 +155,7 @@ export abstract class FeedCache extends EventEmitter { } return "no_change"; })(); - this.#log("Updating %s %s %o", k, updateType, m); + this.log("Updating %s %s %o", k, updateType, m); if (updateType !== "no_change") { const updated = { ...existing, @@ -193,7 +187,7 @@ export abstract class FeedCache extends EventEmitter { "change", fromCache.map(a => this.key(a)), ); - this.#log(`Loaded %d/%d in %d ms`, fromCache.length, keys.length, (unixNowMs() - start).toLocaleString()); + this.log(`Loaded %d/%d in %d ms`, fromCache.length, keys.length, (unixNowMs() - start).toLocaleString()); return mapped.filter(a => !a.has).map(a => a.key); } diff --git a/packages/shared/src/lnurl.ts b/packages/shared/src/lnurl.ts index b9be66fe..64581a63 100644 --- a/packages/shared/src/lnurl.ts +++ b/packages/shared/src/lnurl.ts @@ -136,7 +136,6 @@ export class LNURL { const rsp = await fetch(`${baseUrl}?${queryJoined}`); if (rsp.ok) { const data: LNURLInvoice = await rsp.json(); - console.debug("[LNURL]: ", data); if (data.status === "ERROR") { throw new Error(data.reason); } else { diff --git a/packages/system-react/README.md b/packages/system-react/README.md index b60e70b5..f5e8f73a 100644 --- a/packages/system-react/README.md +++ b/packages/system-react/README.md @@ -4,7 +4,7 @@ React hooks for @snort/system ### Available hooks -#### `useRequestBuilder(NoteStore, RequestBuilder)` +#### `useRequestBuilder(RequestBuilder)` The main hook which allows you to subscribe to nostr relays and returns a reactive store. @@ -63,10 +63,10 @@ export function UserPosts(props: { pubkey: string }) { return rb; }, [props.pubkey]); - const data = useRequestBuilder(NoteCollection, sub); + const data = useRequestBuilder(sub); return ( <> - {data.data.map(a => ( + {data.map(a => ( ))} diff --git a/packages/system-react/example/example.tsx b/packages/system-react/example/example.tsx index f1ebb542..0c553d27 100644 --- a/packages/system-react/example/example.tsx +++ b/packages/system-react/example/example.tsx @@ -1,7 +1,7 @@ import { useMemo } from "react"; import { SnortContext, useRequestBuilder, useUserProfile } from "../src"; -import { NostrSystem, NoteCollection, RequestBuilder, TaggedNostrEvent } from "@snort/system"; +import { NostrSystem, RequestBuilder, TaggedNostrEvent } from "@snort/system"; const System = new NostrSystem({}); @@ -13,7 +13,7 @@ export function Note({ ev }: { ev: TaggedNostrEvent }) { return (
- Post by: {profile.name ?? profile.display_name} + Post by: {profile?.name ?? profile?.display_name}

{ev.content}

); @@ -27,10 +27,10 @@ export function UserPosts(props: { pubkey: string }) { return rb; }, [props.pubkey]); - const data = useRequestBuilder(NoteCollection, sub); + const data = useRequestBuilder(sub); return ( <> - {data.data.map(a => ( + {data.map(a => ( ))} diff --git a/packages/system-react/src/useEventFeed.ts b/packages/system-react/src/useEventFeed.ts index 5e03aa7b..945e06f0 100644 --- a/packages/system-react/src/useEventFeed.ts +++ b/packages/system-react/src/useEventFeed.ts @@ -9,7 +9,7 @@ export function useEventFeed(link: NostrLink) { return b; }, [link]); - return useRequestBuilder(sub); + return useRequestBuilder(sub).at(0); } export function useEventsFeed(id: string, links: Array) { diff --git a/packages/system-react/src/useReactions.ts b/packages/system-react/src/useReactions.ts index 3b2b583e..14e9454d 100644 --- a/packages/system-react/src/useReactions.ts +++ b/packages/system-react/src/useReactions.ts @@ -27,7 +27,7 @@ export function useReactions( } } others?.(rb); - return rb.numFilters > 0 ? rb : null; + return rb.numFilters > 0 ? rb : undefined; }, [ids]); return useRequestBuilder(sub); diff --git a/packages/system-react/src/useRequestBuilder.tsx b/packages/system-react/src/useRequestBuilder.tsx index 11bfa16c..0c4afded 100644 --- a/packages/system-react/src/useRequestBuilder.tsx +++ b/packages/system-react/src/useRequestBuilder.tsx @@ -1,40 +1,55 @@ -import { useContext, useSyncExternalStore } from "react"; -import { RequestBuilder, EmptySnapshot, NoteStore, StoreSnapshot } from "@snort/system"; -import { unwrap } from "@snort/shared"; +import { useCallback, useContext, useEffect, useMemo, useSyncExternalStore } from "react"; +import { EmptySnapshot, RequestBuilder } from "@snort/system"; import { SnortContext } from "./context"; /** * Send a query to the relays and wait for data */ -const useRequestBuilder = ( - rb: RequestBuilder | null, -) => { +export function useRequestBuilder(rb: RequestBuilder | null | undefined) { const system = useContext(SnortContext); - const subscribe = (onChanged: () => void) => { + return useSyncExternalStore( + v => { + if (rb) { + const q = system.Query(rb); + q.on("event", v); + q.uncancel(); + return () => { + q.off("event", v); + q.cancel(); + }; + } + return () => { + // noop + }; + }, + () => { + const q = system.GetQuery(rb?.id ?? ""); + if (q) { + return q.snapshot; + } else { + return EmptySnapshot; + } + }, + ); +} + +/** + * More advanced hook which returns the Query object + */ +export function useRequestBuilderAdvanced(rb: RequestBuilder | null | undefined) { + const system = useContext(SnortContext); + const q = useMemo(() => { if (rb) { const q = system.Query(rb); - q.on("event", onChanged); q.uncancel(); - return () => { - q.off("event", onChanged); - q.cancel(); - }; + return q; } + }, [rb]); + useEffect(() => { return () => { - // noop + q?.cancel(); }; - }; - const getState = () => { - const q = system.GetQuery(rb?.id ?? ""); - if (q) { - return q.snapshot; - } - return EmptySnapshot; - }; - return useSyncExternalStore( - v => subscribe(v), - () => getState(), - ); -}; + }, [q]); -export { useRequestBuilder }; + return q; +} diff --git a/packages/system-svelte/src/request-builder.ts b/packages/system-svelte/src/request-builder.ts index 7202c58c..ff021920 100644 --- a/packages/system-svelte/src/request-builder.ts +++ b/packages/system-svelte/src/request-builder.ts @@ -1,19 +1,19 @@ -import { type NoteStore, type RequestBuilder, type StoreSnapshot, type SystemInterface } from "@snort/system"; +import { TaggedNostrEvent, type RequestBuilder, type SystemInterface } from "@snort/system"; import { getContext } from "svelte"; -export function useRequestBuilder(type: new () => T, rb: RequestBuilder) { +export function useRequestBuilder(rb: RequestBuilder) { const system = getContext("snort") as SystemInterface; - type TSnap = StoreSnapshot>; return { - subscribe: (set: (value: TSnap) => void) => { - const q = system.Query(type, rb); + subscribe: (set: (value: Array) => void) => { + const q = system.Query(rb); + const handle = () => { + set(q.snapshot); + }; q.uncancel(); - const release = q.feed.hook(() => { - set(q.feed.snapshot as TSnap); - }); + q.on("event", handle); return () => { + q.off("event", handle); q.cancel(); - release(); }; }, }; diff --git a/packages/system/README.md b/packages/system/README.md index 0758e410..8a39e11b 100644 --- a/packages/system/README.md +++ b/packages/system/README.md @@ -5,12 +5,7 @@ A collection of caching and querying techniquies used by https://snort.social to Simple example: ```js -import { - NostrSystem, - RequestBuilder, - StoreSnapshot, - NoteCollection -} from "@snort/system" +import { NostrSystem, RequestBuilder, StoreSnapshot, NoteCollection } from "@snort/system"; // Singleton instance to store all connections and access query fetching system const System = new NostrSystem({}); @@ -30,25 +25,11 @@ const System = new NostrSystem({}); .kinds([1]) .limit(10); - const q = System.Query(NoteCollection, rb); + const q = System.Query(rb); // basic usage using "onEvent", fired every 100ms - q.feed.onEvent(evs => { + q.on("event", evs => { console.log(evs); // something else.. }); - - // Hookable type using change notification, limited to every 500ms - const release = q.feed.hook(() => { - // since we use the NoteCollection we expect NostrEvent[] - // other stores provide different data, like a single event instead of an array (latest version) - const state = q.feed.snapshot as StoreSnapshot>; - - // do something with snapshot of store - console.log(`We have ${state.data?.length} events now!`); - }); - - // release the hook when its not needed anymore - // these patterns will be managed in @snort/system-react to make it easier to use react or other UI frameworks - release(); })(); ``` diff --git a/packages/system/examples/simple.ts b/packages/system/examples/simple.ts index 1c7f6804..534cd1bf 100644 --- a/packages/system/examples/simple.ts +++ b/packages/system/examples/simple.ts @@ -18,24 +18,10 @@ const System = new NostrSystem({}); .kinds([1]) .limit(10); - const q = System.Query(NoteCollection, rb); + const q = System.Query(rb); // basic usage using "onEvent", fired every 100ms - q.feed.onEvent(evs => { + q.on("event", evs => { console.log(evs); // something else.. }); - - // Hookable type using change notification, limited to every 500ms - const release = q.feed.hook(() => { - // since we use the FlatNoteStore we expect NostrEvent[] - // other stores provide different data, like a single event instead of an array (latest version) - const state = q.feed.snapshot as StoreSnapshot>; - - // do something with snapshot of store - console.log(`We have ${state.data?.length} events now!`); - }); - - // release the hook when its not needed anymore - // these patterns will be managed in @snort/system-react to make it easier to use react or other UI frameworks - release(); })(); diff --git a/packages/system/src/background-loader.ts b/packages/system/src/background-loader.ts index 5ab617c1..06ea13a6 100644 --- a/packages/system/src/background-loader.ts +++ b/packages/system/src/background-loader.ts @@ -76,16 +76,17 @@ export abstract class BackgroundLoader((resolve, reject) => { this.TrackKeys(key); - this.cache.on("change", keys => { + const handler = (keys: Array) => { if (keys.includes(key)) { const existing = this.cache.getFromCache(key); if (existing) { resolve(existing); this.UntrackKeys(key); - this.cache.off("change"); + this.cache.off("change", handler); } } - }); + }; + this.cache.on("change", handler); }); } } @@ -103,6 +104,7 @@ export abstract class BackgroundLoader !found.some(b => a === this.cache.key(b))).map(a => this.makePlaceholder(a)), ); if (noResult.length > 0) { + this.#log("Adding placeholders for %O", noResult); await Promise.all(noResult.map(a => this.cache.update(a))); } } catch (e) { @@ -115,19 +117,24 @@ export abstract class BackgroundLoader) { + this.#log("Loading data", missing); if (this.loaderFn) { const results = await this.loaderFn(missing); await Promise.all(results.map(a => this.cache.update(a))); return results; } else { + const hookHandled = new Set(); const v = await this.#system.Fetch(this.buildSub(missing), async e => { + this.#log("Callback handled %o", e); for (const pe of e) { const m = this.onEvent(pe); if (m) { await this.cache.update(m); + hookHandled.add(pe.id); } } }); + this.#log("Got data", v); return removeUndefined(v.map(this.onEvent)); } } diff --git a/packages/system/src/cache/index.ts b/packages/system/src/cache/index.ts index f7e327cb..bee1e923 100644 --- a/packages/system/src/cache/index.ts +++ b/packages/system/src/cache/index.ts @@ -50,6 +50,7 @@ export interface UsersRelays { } export function mapEventToProfile(ev: NostrEvent) { + if (ev.kind !== 0) return; try { const data: UserMetadata = JSON.parse(ev.content); let ret = { diff --git a/packages/system/src/nostr-connection-pool.ts b/packages/system/src/connection-pool.ts similarity index 91% rename from packages/system/src/nostr-connection-pool.ts rename to packages/system/src/connection-pool.ts index 2679643f..0b20bc62 100644 --- a/packages/system/src/nostr-connection-pool.ts +++ b/packages/system/src/connection-pool.ts @@ -30,7 +30,9 @@ export type ConnectionPool = { /** * Simple connection pool containing connections to multiple nostr relays */ -export class NostrConnectionPool extends EventEmitter implements ConnectionPool { +export class DefaultConnectionPool extends EventEmitter implements ConnectionPool { + #system: SystemInterface; + #log = debug("NostrConnectionPool"); /** @@ -38,6 +40,11 @@ export class NostrConnectionPool extends EventEmitter */ #sockets = new Map(); + constructor(system: SystemInterface) { + super(); + this.#system = system; + } + /** * Get basic state information from the pool */ @@ -63,7 +70,13 @@ export class NostrConnectionPool extends EventEmitter const c = new Connection(addr, options, ephemeral); this.#sockets.set(addr, c); - c.on("event", (s, e) => this.emit("event", addr, s, e)); + c.on("event", (s, e) => { + if (this.#system.checkSigs && !this.#system.optimizer.schnorrVerify(e)) { + this.#log("Reject invalid event %o", e); + return; + } + this.emit("event", addr, s, e); + }); c.on("eose", s => this.emit("eose", addr, s)); c.on("disconnect", code => this.emit("disconnect", addr, code)); c.on("connected", r => this.emit("connected", addr, r)); diff --git a/packages/system/src/connection.ts b/packages/system/src/connection.ts index e9f2c1d4..255866e5 100644 --- a/packages/system/src/connection.ts +++ b/packages/system/src/connection.ts @@ -9,6 +9,7 @@ import { ConnectionStats } from "./connection-stats"; import { NostrEvent, ReqCommand, ReqFilter, TaggedNostrEvent, u256 } from "./nostr"; import { RelayInfo } from "./relay-info"; import EventKind from "./event-kind"; +import { EventExt } from "./event-ext"; /** * Relay settings @@ -225,10 +226,16 @@ export class Connection extends EventEmitter { break; } case "EVENT": { - this.emit("event", msg[1] as string, { + const ev = { ...(msg[2] as NostrEvent), relays: [this.Address], - }); + } as TaggedNostrEvent; + + if (!EventExt.isValid(ev)) { + //this.#log("Rejecting invalid event %O", ev); + return; + } + this.emit("event", msg[1] as string, ev); this.Stats.EventsReceived++; this.notifyChange(); break; diff --git a/packages/system/src/index.ts b/packages/system/src/index.ts index e0730ebf..327eebe4 100644 --- a/packages/system/src/index.ts +++ b/packages/system/src/index.ts @@ -1,13 +1,14 @@ import { RelaySettings, ConnectionStateSnapshot, OkResponse } from "./connection"; import { RequestBuilder } from "./request-builder"; -import { NoteCollection, NoteStore, NoteStoreSnapshotData, StoreSnapshot } from "./note-collection"; import { NostrEvent, ReqFilter, TaggedNostrEvent } from "./nostr"; import { ProfileLoaderService } from "./profile-cache"; import { RelayCache, RelayMetadataLoader } from "./outbox-model"; import { Optimizer } from "./query-optimizer"; import { base64 } from "@scure/base"; import { FeedCache } from "@snort/shared"; -import { ConnectionPool } from "nostr-connection-pool"; +import { ConnectionPool } from "./connection-pool"; +import EventEmitter from "eventemitter3"; +import { QueryEvents } from "./query"; export { NostrSystem } from "./nostr-system"; export { default as EventKind } from "./event-kind"; @@ -46,13 +47,16 @@ export * from "./cache/relay-metric"; export * from "./worker/system-worker"; -export interface QueryLike { - on: (event: "event", fn?: (evs: Array) => void) => void; - off: (event: "event", fn?: (evs: Array) => void) => void; +export type QueryLike = { + get progress(): number; + feed: { + add: (evs: Array) => void; + clear: () => void; + }; cancel: () => void; uncancel: () => void; - get snapshot(): StoreSnapshot>; -} + get snapshot(): Array; +} & EventEmitter; export interface SystemInterface { /** @@ -87,7 +91,7 @@ export interface SystemInterface { * @param req Request to send to relays * @param cb A callback which will fire every 100ms when new data is received */ - Fetch(req: RequestBuilder, cb?: (evs: ReadonlyArray) => void): Promise>; + Fetch(req: RequestBuilder, cb?: (evs: Array) => void): Promise>; /** * Create a new permanent connection to a relay diff --git a/packages/system/src/nostr-system.ts b/packages/system/src/nostr-system.ts index 726d1524..c51643ac 100644 --- a/packages/system/src/nostr-system.ts +++ b/packages/system/src/nostr-system.ts @@ -23,8 +23,8 @@ import { import { EventsCache } from "./cache/events"; import { RelayMetadataLoader } from "./outbox-model"; import { Optimizer, DefaultOptimizer } from "./query-optimizer"; -import { NostrConnectionPool } from "./nostr-connection-pool"; -import { NostrQueryManager } from "./nostr-query-manager"; +import { ConnectionPool, DefaultConnectionPool } from "./connection-pool"; +import { QueryManager } from "./query-manager"; export interface NostrSystemEvents { change: (state: SystemSnapshot) => void; @@ -48,7 +48,7 @@ export interface NostrsystemProps { */ export class NostrSystem extends EventEmitter implements SystemInterface { #log = debug("System"); - #queryManager: NostrQueryManager; + #queryManager: QueryManager; /** * Storage class for user relay lists @@ -80,7 +80,7 @@ export class NostrSystem extends EventEmitter implements Syst */ readonly optimizer: Optimizer; - readonly pool = new NostrConnectionPool(); + readonly pool: ConnectionPool; readonly eventsCache: FeedCache; readonly relayLoader: RelayMetadataLoader; @@ -102,7 +102,8 @@ export class NostrSystem extends EventEmitter implements Syst this.relayLoader = new RelayMetadataLoader(this, this.relayCache); this.checkSigs = props.checkSigs ?? true; - this.#queryManager = new NostrQueryManager(this); + this.pool = new DefaultConnectionPool(this); + this.#queryManager = new QueryManager(this); // hook connection pool this.pool.on("connected", (id, wasReconnect) => { @@ -121,18 +122,6 @@ export class NostrSystem extends EventEmitter implements Syst }); this.pool.on("event", (_, sub, ev) => { ev.relays?.length && this.relayMetricsHandler.onEvent(ev.relays[0]); - - if (!EventExt.isValid(ev)) { - this.#log("Rejecting invalid event %O", ev); - return; - } - if (this.checkSigs) { - if (!this.optimizer.schnorrVerify(ev)) { - this.#log("Invalid sig %O", ev); - return; - } - } - this.emit("event", sub, ev); }); this.pool.on("disconnect", (id, code) => { @@ -160,17 +149,6 @@ export class NostrSystem extends EventEmitter implements Syst this.#queryManager.on("trace", t => { this.relayMetricsHandler.onTraceReport(t); }); - - // internal handler for on-event - this.on("event", (sub, ev) => { - for (const [, v] of this.#queryManager) { - const trace = v.handleEvent(sub, ev); - // inject events to cache if query by id - if (trace && trace.filters.some(a => a.ids)) { - this.eventsCache.set(ev); - } - } - }); } get Sockets(): ConnectionStateSnapshot[] { @@ -200,15 +178,15 @@ export class NostrSystem extends EventEmitter implements Syst } GetQuery(id: string): QueryLike | undefined { - return this.#queryManager.get(id) as QueryLike; + return this.#queryManager.get(id); } - Fetch(req: RequestBuilder, cb?: (evs: ReadonlyArray) => void) { + Fetch(req: RequestBuilder, cb?: (evs: Array) => void) { return this.#queryManager.fetch(req, cb); } Query(req: RequestBuilder): QueryLike { - return this.#queryManager.query(req) as QueryLike; + return this.#queryManager.query(req); } HandleEvent(ev: TaggedNostrEvent) { diff --git a/packages/system/src/note-collection.ts b/packages/system/src/note-collection.ts index f24b5261..5c31e2e9 100644 --- a/packages/system/src/note-collection.ts +++ b/packages/system/src/note-collection.ts @@ -1,27 +1,10 @@ -import { appendDedupe, SortedMap } from "@snort/shared"; -import { EventExt, EventType, TaggedNostrEvent, u256 } from "."; +import { SortedMap } from "@snort/shared"; +import { EventExt, EventType, TaggedNostrEvent } from "."; import { findTag } from "./utils"; import EventEmitter from "eventemitter3"; -export interface StoreSnapshot { - data: TSnapshot | undefined; - clear: () => void; - loading: () => boolean; - add: (ev: Readonly | Readonly>) => void; -} - -export const EmptySnapshot = { - data: undefined, - clear: () => { - // empty - }, - loading: () => true, - add: () => { - // empty - }, -} as StoreSnapshot>; - -export type NoteStoreSnapshotData = Array | TaggedNostrEvent; +export const EmptySnapshot: NoteStoreSnapshotData = []; +export type NoteStoreSnapshotData = Array; export type NoteStoreHook = () => void; export type NoteStoreHookRelease = () => void; export type OnEventCallback = (e: Readonly>) => void; @@ -30,8 +13,7 @@ export type OnEoseCallback = (c: string) => void; export type OnEoseCallbackRelease = () => void; export interface NosteStoreEvents { - progress: (loading: boolean) => void; - event: (evs: Readonly>) => void; + event: (evs: Array) => void; } /** @@ -40,133 +22,40 @@ export interface NosteStoreEvents { export abstract class NoteStore extends EventEmitter { abstract add(ev: Readonly | Readonly>): void; abstract clear(): void; - abstract getSnapshotData(): NoteStoreSnapshotData | undefined; - abstract get snapshot(): StoreSnapshot; - abstract get loading(): boolean; - abstract set loading(v: boolean); + abstract get snapshot(): NoteStoreSnapshotData; } -export abstract class HookedNoteStore extends NoteStore { - #loading = true; - #storeSnapshot: StoreSnapshot = { - clear: () => this.clear(), - loading: () => this.loading, - add: ev => this.add(ev), - data: undefined, - }; - #needsSnapshot = true; - #nextNotifyTimer?: ReturnType; +export abstract class HookedNoteStore extends NoteStore { + #storeSnapshot: NoteStoreSnapshotData = []; + #nextEmit?: ReturnType; #bufEmit: Array = []; get snapshot() { - this.#updateSnapshot(); return this.#storeSnapshot; } - get loading() { - return this.#loading; - } - - set loading(v: boolean) { - this.#loading = v; - this.emit("progress", v); - } - abstract override add(ev: Readonly | Readonly>): void; abstract override clear(): void; + protected abstract takeSnapshot(): NoteStoreSnapshotData | undefined; - getSnapshotData() { - this.#updateSnapshot(); - return this.#storeSnapshot.data; - } - - protected abstract takeSnapshot(): TSnapshot | undefined; - - protected onChange(changes: Readonly>): void { - this.#needsSnapshot = true; + protected onChange(changes: Array): void { + this.#storeSnapshot = this.takeSnapshot() ?? []; this.#bufEmit.push(...changes); - if (!this.#nextNotifyTimer) { - this.#nextNotifyTimer = setTimeout(() => { - this.#nextNotifyTimer = undefined; + if (!this.#nextEmit) { + this.#nextEmit = setTimeout(() => { + this.#nextEmit = undefined; this.emit("event", this.#bufEmit); this.#bufEmit = []; - }, 500); + }, 300); } } - - #updateSnapshot() { - if (this.#needsSnapshot) { - this.#storeSnapshot = { - ...this.#storeSnapshot, - data: this.takeSnapshot(), - }; - this.#needsSnapshot = false; - } - } -} - -/** - * A store which doesnt store anything, useful for hooks only - */ -export class NoopStore extends HookedNoteStore> { - override add(ev: readonly TaggedNostrEvent[] | Readonly): void { - this.onChange(Array.isArray(ev) ? ev : [ev]); - } - - override clear(): void { - // nothing to do - } - - protected override takeSnapshot(): TaggedNostrEvent[] | undefined { - // nothing to do - return undefined; - } -} - -/** - * A simple flat container of events with no duplicates - */ -export class FlatNoteStore extends HookedNoteStore> { - #events: Array = []; - #ids: Set = new Set(); - - add(ev: TaggedNostrEvent | Array) { - ev = Array.isArray(ev) ? ev : [ev]; - const changes: Array = []; - ev.forEach(a => { - if (!this.#ids.has(a.id)) { - this.#events.push(a); - this.#ids.add(a.id); - changes.push(a); - } else { - const existing = this.#events.findIndex(b => b.id === a.id); - if (existing !== -1) { - this.#events[existing].relays = appendDedupe(this.#events[existing].relays, a.relays); - } - } - }); - - if (changes.length > 0) { - this.onChange(changes); - } - } - - clear() { - this.#events = []; - this.#ids.clear(); - this.onChange([]); - } - - takeSnapshot() { - return [...this.#events]; - } } /** * A note store that holds a single replaceable event for a given user defined key generator function */ -export class KeyedReplaceableNoteStore extends HookedNoteStore> { +export class KeyedReplaceableNoteStore extends HookedNoteStore { #keyFn: (ev: TaggedNostrEvent) => string; #events: SortedMap = new SortedMap([], (a, b) => b[1].created_at - a[1].created_at); @@ -201,39 +90,6 @@ export class KeyedReplaceableNoteStore extends HookedNoteStore> { - #event?: TaggedNostrEvent; - - add(ev: TaggedNostrEvent | Array) { - ev = Array.isArray(ev) ? ev : [ev]; - const changes: Array = []; - ev.forEach(a => { - const existingCreated = this.#event?.created_at ?? 0; - if (a.created_at > existingCreated) { - this.#event = a; - changes.push(a); - } - }); - if (changes.length > 0) { - this.onChange(changes); - } - } - - clear() { - this.#event = undefined; - this.onChange([]); - } - - takeSnapshot() { - if (this.#event) { - return { ...this.#event }; - } - } -} - /** * General use note store based on kind ranges */ diff --git a/packages/system/src/profile-cache.ts b/packages/system/src/profile-cache.ts index 377d61ac..07b8e983 100644 --- a/packages/system/src/profile-cache.ts +++ b/packages/system/src/profile-cache.ts @@ -2,7 +2,6 @@ import { unixNowMs } from "@snort/shared"; import { EventKind, TaggedNostrEvent, RequestBuilder } from "."; import { ProfileCacheExpire } from "./const"; import { mapEventToProfile, CachedMetadata } from "./cache"; -import { v4 as uuid } from "uuid"; import { BackgroundLoader } from "./background-loader"; export class ProfileLoaderService extends BackgroundLoader { @@ -19,14 +18,8 @@ export class ProfileLoaderService extends BackgroundLoader { } override buildSub(missing: string[]): RequestBuilder { - const sub = new RequestBuilder(`profiles-${uuid()}`); - sub - .withOptions({ - skipDiff: true, - }) - .withFilter() - .kinds([EventKind.SetMetadata]) - .authors(missing); + const sub = new RequestBuilder(`profiles`); + sub.withFilter().kinds([EventKind.SetMetadata]).authors(missing); return sub; } diff --git a/packages/system/src/nostr-query-manager.ts b/packages/system/src/query-manager.ts similarity index 78% rename from packages/system/src/nostr-query-manager.ts rename to packages/system/src/query-manager.ts index 9647b8b3..1d858abb 100644 --- a/packages/system/src/nostr-query-manager.ts +++ b/packages/system/src/query-manager.ts @@ -2,11 +2,10 @@ import debug from "debug"; import EventEmitter from "eventemitter3"; import { BuiltRawReqFilter, RequestBuilder, SystemInterface, TaggedNostrEvent } from "."; import { Query, TraceReport } from "./query"; -import { unwrap } from "@snort/shared"; import { FilterCacheLayer, IdsFilterCacheLayer } from "./filter-cache-layer"; import { trimFilters } from "./request-trim"; -interface NostrQueryManagerEvents { +interface QueryManagerEvents { change: () => void; trace: (report: TraceReport) => void; } @@ -14,8 +13,8 @@ interface NostrQueryManagerEvents { /** * Query manager handles sending requests to the nostr network */ -export class NostrQueryManager extends EventEmitter { - #log = debug("NostrQueryManager"); +export class QueryManager extends EventEmitter { + #log = debug("QueryManager"); /** * All active queries @@ -70,20 +69,27 @@ export class NostrQueryManager extends EventEmitter { /** * Async fetch results */ - fetch(req: RequestBuilder, cb?: (evs: ReadonlyArray) => void) { - const q = this.query(req); - return new Promise>(resolve => { - if (cb) { - q.feed.on("event", cb); - } - q.feed.on("progress", loading => { + async fetch(req: RequestBuilder, cb?: (evs: Array) => void) { + const q = new Query(this.#system, req); + q.on("trace", r => this.emit("trace", r)); + q.on("filters", fx => { + this.#send(q, fx); + }); + if (cb) { + q.on("event", evs => cb(evs)); + } + await new Promise(resolve => { + q.on("loading", loading => { + this.#log("loading %s %o", q.id, loading); if (!loading) { - q.feed.off("event"); - q.cancel(); - resolve(unwrap(q.snapshot.data)); + resolve(); } }); }); + const results = q.feed.takeSnapshot(); + q.cleanup(); + this.#log("Fetch results for %s %o", q.id, results); + return results; } *[Symbol.iterator]() { @@ -105,12 +111,13 @@ export class NostrQueryManager extends EventEmitter { // check for empty filters const fNew = trimFilters(qSend.filters); if (fNew.length === 0) { + this.#log("Dropping %s %o", q.id, qSend); return; } qSend.filters = fNew; if (qSend.relay) { - this.#log("Sending query to %s %O", qSend.relay, qSend); + this.#log("Sending query to %s %s %O", qSend.relay, q.id, qSend); const s = this.#system.pool.getConnection(qSend.relay); if (s) { const qt = q.sendToRelay(s, qSend); @@ -132,7 +139,7 @@ export class NostrQueryManager extends EventEmitter { const ret = []; for (const [a, s] of this.#system.pool) { if (!s.Ephemeral) { - this.#log("Sending query to %s %O", a, qSend); + this.#log("Sending query to %s %s %O", a, q.id, qSend); const qt = q.sendToRelay(s, qSend); if (qt) { ret.push(qt); @@ -142,6 +149,7 @@ export class NostrQueryManager extends EventEmitter { return ret; } } + #cleanup() { let changed = false; for (const [k, v] of this.#queries) { diff --git a/packages/system/src/query.ts b/packages/system/src/query.ts index 6a8451dc..ffdd5975 100644 --- a/packages/system/src/query.ts +++ b/packages/system/src/query.ts @@ -4,7 +4,7 @@ import EventEmitter from "eventemitter3"; import { unixNowMs, unwrap } from "@snort/shared"; import { Connection, ReqFilter, Nips, TaggedNostrEvent, SystemInterface } from "."; -import { NoteCollection, NoteStore } from "./note-collection"; +import { NoteCollection } from "./note-collection"; import { BuiltRawReqFilter, RequestBuilder } from "./request-builder"; import { eventMatchesFilter } from "./request-matcher"; @@ -97,25 +97,26 @@ export interface TraceReport { responseTime: number; } -interface QueryEvents { +export interface QueryEvents { + loading: (v: boolean) => void; trace: (report: TraceReport) => void; filters: (req: BuiltRawReqFilter) => void; - event: (evs: ReadonlyArray) => void; + event: (evs: Array) => void; + end: () => void; } /** * Active or queued query on the system */ export class Query extends EventEmitter { - /** - * Unique id of this query - */ - readonly id: string; + get id() { + return this.request.id; + } /** * RequestBuilder instance */ - requests: Array = []; + request: RequestBuilder; /** * Nostr system interface @@ -166,8 +167,7 @@ export class Query extends EventEmitter { constructor(system: SystemInterface, req: RequestBuilder) { super(); - this.id = uuid(); - this.requests.push(req); + this.request = req; this.#system = system; this.#feed = new NoteCollection(); this.#leaveOpen = req.options?.leaveOpen ?? false; @@ -176,32 +176,21 @@ export class Query extends EventEmitter { this.#checkTraces(); this.feed.on("event", evs => this.emit("event", evs)); + this.#start(); } /** * Adds another request to this one */ addRequest(req: RequestBuilder) { - if (this.#groupTimeout) { - clearTimeout(this.#groupTimeout); - this.#groupTimeout = undefined; - } - if (this.requests.some(a => a.instance === req.instance)) { - // already exists, nothing to add - return false; - } - if (this.requests.some(a => a.options?.skipDiff !== req.options?.skipDiff)) { - throw new Error("Mixing skipDiff option is not supported"); - } - this.requests.push(req); - - if (this.#groupingDelay) { - this.#groupTimeout = setTimeout(() => { - this.#emitFilters(); - }, this.#groupingDelay); - } else { - this.#emitFilters(); + if (req.instance === this.request.instance) { + // same requst, do nothing + this.#log("Same query %O === %O", req, this.request); + return; } + this.#log("Add query %O to %s", req, this.id); + this.request.add(req); + this.#start(); return true; } @@ -228,12 +217,11 @@ export class Query extends EventEmitter { return this.#feed.snapshot; } - handleEvent(sub: string, e: TaggedNostrEvent) { + #handleEvent(sub: string, e: TaggedNostrEvent) { for (const t of this.#tracing) { if (t.id === sub || sub === "*") { if (t.filters.some(v => eventMatchesFilter(e, v))) { this.feed.add(e); - return t; } else { this.#log("Event did not match filter, rejecting %O %O", e, t); } @@ -254,7 +242,12 @@ export class Query extends EventEmitter { } cleanup() { + if (this.#groupTimeout) { + clearTimeout(this.#groupTimeout); + this.#groupTimeout = undefined; + } this.#stopCheckTraces(); + this.emit("end"); } /** @@ -316,35 +309,46 @@ export class Query extends EventEmitter { return thisProgress; } - #emitFilters() { - if (this.requests.every(a => !!a.options?.skipDiff)) { - const existing = this.filters; - const rb = new RequestBuilder(this.id); - this.requests.forEach(a => rb.add(a)); - const filters = rb.buildDiff(this.#system, existing); - filters.forEach(f => this.emit("filters", f)); - this.requests = []; + #start() { + if (this.#groupTimeout) { + clearTimeout(this.#groupTimeout); + this.#groupTimeout = undefined; + } + if (this.#groupingDelay) { + this.#groupTimeout = setTimeout(() => { + this.#emitFilters(); + }, this.#groupingDelay); } else { - // send without diff - const rb = new RequestBuilder(this.id); - this.requests.forEach(a => rb.add(a)); - const filters = rb.build(this.#system); + this.#emitFilters(); + } + } + + #emitFilters() { + this.#log("Starting emit of %s", this.id); + const existing = this.filters; + if (!(this.request.options?.skipDiff ?? false) && existing.length > 0) { + const filters = this.request.buildDiff(this.#system, existing); + this.#log("Build %s %O", this.id, filters); + filters.forEach(f => this.emit("filters", f)); + } else { + const filters = this.request.build(this.#system); + this.#log("Build %s %O", this.id, filters); filters.forEach(f => this.emit("filters", f)); - this.requests = []; } } #onProgress() { const isFinished = this.progress === 1; - if (this.feed.loading !== isFinished) { - this.#log("%s loading=%s, progress=%d, traces=%O", this.id, this.feed.loading, this.progress, this.#tracing); - this.feed.loading = isFinished; + if (isFinished) { + this.#log("%s loading=%s, progress=%d, traces=%O", this.id, !isFinished, this.progress, this.#tracing); + this.emit("loading", !isFinished); } } #stopCheckTraces() { if (this.#checkTrace) { clearInterval(this.#checkTrace); + this.#checkTrace = undefined; } } @@ -398,6 +402,9 @@ export class Query extends EventEmitter { responseTime: qt.responseTime, } as TraceReport), ); + const handler = (sub: string, ev: TaggedNostrEvent) => this.#handleEvent(sub, ev); + c.on("event", handler); + this.on("end", () => c.off("event", handler)); this.#tracing.push(qt); c.QueueReq(["REQ", qt.id, ...qt.filters], () => qt.sentToRelay()); return qt; diff --git a/packages/system/src/worker/system-worker.ts b/packages/system/src/worker/system-worker.ts index bcd9371d..543e64cc 100644 --- a/packages/system/src/worker/system-worker.ts +++ b/packages/system/src/worker/system-worker.ts @@ -25,7 +25,7 @@ import { FeedCache } from "@snort/shared"; import { EventsCache } from "../cache/events"; import { RelayMetricHandler } from "../relay-metric-handler"; import debug from "debug"; -import { ConnectionPool } from "nostr-connection-pool"; +import { ConnectionPool } from "connection-pool"; export class SystemWorker extends EventEmitter implements SystemInterface { #log = debug("SystemWorker");