diff --git a/packages/app/package.json b/packages/app/package.json index d1d006248..d3a6454d4 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -12,6 +12,7 @@ "@scure/bip39": "^1.1.1", "@snort/shared": "workspace:*", "@snort/system": "workspace:*", + "@snort/system-query": "workspace:*", "@snort/system-react": "workspace:*", "@szhsin/react-menu": "^3.3.1", "@void-cat/api": "^1.0.4", @@ -101,6 +102,7 @@ "prop-types": "^15.8.1", "source-map-loader": "^4.0.1", "terser-webpack-plugin": "^5.3.9", + "tinybench": "^2.5.0", "ts-jest": "^29.1.0", "ts-loader": "^9.4.4", "typescript": "^5.2.2", diff --git a/packages/app/public/bench.html b/packages/app/public/bench.html new file mode 100644 index 000000000..95159cd91 --- /dev/null +++ b/packages/app/public/bench.html @@ -0,0 +1,10 @@ + + + + + Snort Benchmarks + + + Check console + + diff --git a/packages/app/public/index.html b/packages/app/public/index.html index ee84c3b73..b5a138322 100644 --- a/packages/app/public/index.html +++ b/packages/app/public/index.html @@ -1,4 +1,4 @@ - + diff --git a/packages/app/src/Cache/GiftWrapCache.ts b/packages/app/src/Cache/GiftWrapCache.ts index 71e82b457..67f00b917 100644 --- a/packages/app/src/Cache/GiftWrapCache.ts +++ b/packages/app/src/Cache/GiftWrapCache.ts @@ -38,7 +38,7 @@ export class GiftWrapCache extends RefreshFeedCache { } catch (e) { console.debug(e, v); } - }) + }), ) ) .filter(a => a !== undefined) diff --git a/packages/app/src/Element/CashuNuts.tsx b/packages/app/src/Element/CashuNuts.tsx index 8674657b7..4e124e389 100644 --- a/packages/app/src/Element/CashuNuts.tsx +++ b/packages/app/src/Element/CashuNuts.tsx @@ -26,7 +26,7 @@ export default function CashuNuts({ token }: { token: string }) { e.stopPropagation(); const lnurl = profile?.lud16 ?? ""; const url = `https://redeem.cashu.me?token=${encodeURIComponent(token)}&lightning=${encodeURIComponent( - lnurl + lnurl, )}&autopay=yes`; window.open(url, "_blank"); } diff --git a/packages/app/src/Element/FollowButton.tsx b/packages/app/src/Element/FollowButton.tsx index 64a1dc38e..c2cb0e54f 100644 --- a/packages/app/src/Element/FollowButton.tsx +++ b/packages/app/src/Element/FollowButton.tsx @@ -34,7 +34,7 @@ export default function FollowButton(props: FollowButtonProps) { if (publisher) { const ev = await publisher.contactList( follows.item.filter(a => a !== pubkey), - relays.item + relays.item, ); System.BroadcastEvent(ev); } diff --git a/packages/app/src/Element/Nip5Service.tsx b/packages/app/src/Element/Nip5Service.tsx index 19318901a..5a389c445 100644 --- a/packages/app/src/Element/Nip5Service.tsx +++ b/packages/app/src/Element/Nip5Service.tsx @@ -261,9 +261,7 @@ export default function Nip5Service(props: Nip05ServiceProps) { />  @  )} diff --git a/packages/app/src/Element/Note.tsx b/packages/app/src/Element/Note.tsx index db857738b..85deb9dc8 100644 --- a/packages/app/src/Element/Note.tsx +++ b/packages/app/src/Element/Note.tsx @@ -135,7 +135,7 @@ export function NoteInner(props: NoteProps) { { [Reaction.Positive]: [] as TaggedNostrEvent[], [Reaction.Negative]: [] as TaggedNostrEvent[], - } + }, ); return { [Reaction.Positive]: dedupeByPubkey(result[Reaction.Positive]), @@ -150,7 +150,7 @@ export function NoteInner(props: NoteProps) { ...getReactions(related, ev.id, EventKind.TextNote).filter(e => e.tags.some(tagFilterOfTextRepost(e, ev.id))), ...getReactions(related, ev.id, EventKind.Repost), ]), - [related, ev] + [related, ev], ); const zaps = useMemo(() => { const sortedZaps = getReactions(related, ev.id, EventKind.ZapReceipt) @@ -241,7 +241,7 @@ export function NoteInner(props: NoteProps) { function goToEvent( e: React.MouseEvent, eTarget: TaggedNostrEvent, - isTargetAllowed: boolean = e.target === e.currentTarget + isTargetAllowed: boolean = e.target === e.currentTarget, ) { if (!isTargetAllowed || opt?.canClick === false) { return; diff --git a/packages/app/src/Element/NoteCreator.css b/packages/app/src/Element/NoteCreator.css index e879558e6..444170171 100644 --- a/packages/app/src/Element/NoteCreator.css +++ b/packages/app/src/Element/NoteCreator.css @@ -2,7 +2,8 @@ border: 1px solid transparent; border-radius: 12px; box-shadow: 0px 0px 6px 1px rgba(182, 108, 156, 0.3); - background: linear-gradient(var(--gray-superdark), var(--gray-superdark)) padding-box, + background: + linear-gradient(var(--gray-superdark), var(--gray-superdark)) padding-box, linear-gradient(90deg, #ef9644, #fd7c49, #ff5e58, #ff3b70, #ff088e, #eb00b1, #c31ed5, #7b41f6) border-box; } diff --git a/packages/app/src/Element/NoteCreator.tsx b/packages/app/src/Element/NoteCreator.tsx index d9a5ab854..3ea2e7dd6 100644 --- a/packages/app/src/Element/NoteCreator.tsx +++ b/packages/app/src/Element/NoteCreator.tsx @@ -76,8 +76,8 @@ export function NoteCreator() { setError( formatMessage({ defaultMessage: "Invalid LNURL", - }) - ) + }), + ), ); return; } @@ -256,9 +256,9 @@ export function NoteCreator() { ? false : // otherwise return selectedCustomRelays with target relay added / removed a.filter(el => - el === r ? e.target.checked : !selectedCustomRelays || selectedCustomRelays.includes(el) - ) - ) + el === r ? e.target.checked : !selectedCustomRelays || selectedCustomRelays.includes(el), + ), + ), ) } /> diff --git a/packages/app/src/Element/NoteFooter.tsx b/packages/app/src/Element/NoteFooter.tsx index 6e74f1f1a..068c8ac50 100644 --- a/packages/app/src/Element/NoteFooter.tsx +++ b/packages/app/src/Element/NoteFooter.tsx @@ -71,7 +71,7 @@ export default function NoteFooter(props: NoteFooterProps) { }, { captureEvent: true, - } + }, ); function hasReacted(emoji: string) { diff --git a/packages/app/src/Element/NoteReaction.tsx b/packages/app/src/Element/NoteReaction.tsx index 66f19abb9..a2992c7f5 100644 --- a/packages/app/src/Element/NoteReaction.tsx +++ b/packages/app/src/Element/NoteReaction.tsx @@ -10,6 +10,7 @@ import useModeration from "Hooks/useModeration"; import { FormattedMessage } from "react-intl"; import Icon from "Icons/Icon"; import { useUserProfile } from "@snort/system-react"; +import { useInView } from "react-intersection-observer"; export interface NoteReactionProps { data: TaggedNostrEvent; @@ -19,7 +20,8 @@ export interface NoteReactionProps { export default function NoteReaction(props: NoteReactionProps) { const { data: ev } = props; const { isMuted } = useModeration(); - const profile = useUserProfile(ev.pubkey); + const { inView, ref } = useInView({ triggerOnce: true }); + const profile = useUserProfile(inView ? ev.pubkey : ""); const refEvent = useMemo(() => { if (ev) { @@ -44,6 +46,7 @@ export default function NoteReaction(props: NoteReactionProps) { * Some clients embed the reposted note in the content */ function extractRoot() { + if (!inView) return null; if (ev?.kind === EventKind.Repost && ev.content.length > 0 && ev.content !== "#[0]") { try { const r: NostrEvent = JSON.parse(ev.content); @@ -60,7 +63,11 @@ export default function NoteReaction(props: NoteReactionProps) { return props.root; } - const root = extractRoot(); + const root = useMemo(() => extractRoot(), [ev, props.root, inView]); + + if (!inView) { + return
; + } const isOpMuted = root && isMuted(root.pubkey); const shouldNotBeRendered = isOpMuted || root?.kind !== EventKind.TextNote; const opt = { diff --git a/packages/app/src/Element/Poll.tsx b/packages/app/src/Element/Poll.tsx index a72cf2cd9..d1c163db7 100644 --- a/packages/app/src/Element/Poll.tsx +++ b/packages/app/src/Element/Poll.tsx @@ -47,15 +47,15 @@ export default function Poll(props: PollProps) { }, { amount, - } - ) + }, + ), ); } setVoting(opt); const r = Object.keys(relays.item); const zap = await publisher.zap(amount * 1000, props.ev.pubkey, r, props.ev.id, undefined, eb => - eb.tag(["poll_option", opt.toString()]) + eb.tag(["poll_option", opt.toString()]), ); const lnurl = props.ev.tags.find(a => a[0] === "zap")?.[1] || pollerProfile?.lud16 || pollerProfile?.lud06; @@ -68,7 +68,7 @@ export default function Poll(props: PollProps) { throw new Error( formatMessage({ defaultMessage: "Can't vote because LNURL service does not support zaps", - }) + }), ); } @@ -85,7 +85,7 @@ export default function Poll(props: PollProps) { setError( formatMessage({ defaultMessage: "Failed to send vote", - }) + }), ); } } finally { diff --git a/packages/app/src/Element/PubkeyList.tsx b/packages/app/src/Element/PubkeyList.tsx index 176484339..8a5d14dfa 100644 --- a/packages/app/src/Element/PubkeyList.tsx +++ b/packages/app/src/Element/PubkeyList.tsx @@ -34,7 +34,7 @@ export default function PubkeyList({ ev, className }: { ev: NostrEvent; classNam pk, Object.keys(login.relays.item), undefined, - `Zap from ${hexToBech32("note", ev.id)}` + `Zap from ${hexToBech32("note", ev.id)}`, ); const invoice = await svc.getInvoice(amtSend, undefined, zap); if (invoice.pr) { diff --git a/packages/app/src/Element/ReBroadcaster.tsx b/packages/app/src/Element/ReBroadcaster.tsx index b3ad9dabc..53029aebe 100644 --- a/packages/app/src/Element/ReBroadcaster.tsx +++ b/packages/app/src/Element/ReBroadcaster.tsx @@ -55,9 +55,9 @@ export function ReBroadcaster() { ? false : // otherwise return selectedCustomRelays with target relay added / removed a.filter(el => - el === r ? e.target.checked : !selectedCustomRelays || selectedCustomRelays.includes(el) - ) - ) + el === r ? e.target.checked : !selectedCustomRelays || selectedCustomRelays.includes(el), + ), + ), ) } /> diff --git a/packages/app/src/Element/Reactions.tsx b/packages/app/src/Element/Reactions.tsx index cd755b26e..076089c27 100644 --- a/packages/app/src/Element/Reactions.tsx +++ b/packages/app/src/Element/Reactions.tsx @@ -60,7 +60,7 @@ const Reactions = ({ show, setShow, positive, negative, reposts, zaps }: Reactio value: 3, }, ] - : [] + : [], ); const [tab, setTab] = useState(tabs[0]); diff --git a/packages/app/src/Element/Relay.tsx b/packages/app/src/Element/Relay.tsx index 1fca237aa..0de4d2888 100644 --- a/packages/app/src/Element/Relay.tsx +++ b/packages/app/src/Element/Relay.tsx @@ -22,7 +22,7 @@ export default function Relay(props: RelayProps) { const navigate = useNavigate(); const login = useLogin(); const relaySettings = unwrap( - login.relays.item[props.addr] ?? System.Sockets.find(a => a.address === props.addr)?.settings ?? {} + login.relays.item[props.addr] ?? System.Sockets.find(a => a.address === props.addr)?.settings ?? {}, ); const state = useRelayState(props.addr); const name = useMemo(() => getRelayName(props.addr), [props.addr]); @@ -34,7 +34,7 @@ export default function Relay(props: RelayProps) { ...login.relays.item, [props.addr]: o, }, - unixNowMs() + unixNowMs(), ); } diff --git a/packages/app/src/Element/SendSats.tsx b/packages/app/src/Element/SendSats.tsx index 368095d9e..1c0823f01 100644 --- a/packages/app/src/Element/SendSats.tsx +++ b/packages/app/src/Element/SendSats.tsx @@ -285,7 +285,7 @@ export default function SendSats(props: SendSatsProps) { {makeTab(ZapType.AnonZap, )} {makeTab( ZapType.NonZap, - + , )} diff --git a/packages/app/src/Element/Thread.tsx b/packages/app/src/Element/Thread.tsx index ee6512b54..73532db7d 100644 --- a/packages/app/src/Element/Thread.tsx +++ b/packages/app/src/Element/Thread.tsx @@ -243,7 +243,7 @@ export default function Thread() { if (t?.root?.key === "a" && t?.root?.value) { const parsed = t.root.value.split(":"); replyTo = thread.data?.find( - a => a.kind === Number(parsed[0]) && a.pubkey === parsed[1] && findTag(a, "d") === parsed[2] + a => a.kind === Number(parsed[0]) && a.pubkey === parsed[1] && findTag(a, "d") === parsed[2], )?.id; } if (replyTo) { @@ -264,7 +264,7 @@ export default function Thread() { thread.data?.find( ne => ne.id === currentId || - (link.type === NostrPrefix.Address && findTag(ne, "d") === currentId && ne.pubkey === link.author) + (link.type === NostrPrefix.Address && findTag(ne, "d") === currentId && ne.pubkey === link.author), ) ?? (location.state && "sig" in location.state ? (location.state as TaggedNostrEvent) : undefined); if (currentNote) { const currentThread = EventExt.extractThread(currentNote); @@ -280,7 +280,7 @@ export default function Thread() { if (replyTo.key === "a" && replyTo.value) { const parsed = replyTo.value.split(":"); return thread.data?.find( - a => a.kind === Number(parsed[0]) && a.pubkey === parsed[1] && findTag(a, "d") === parsed[2] + a => a.kind === Number(parsed[0]) && a.pubkey === parsed[1] && findTag(a, "d") === parsed[2], ); } if (replyTo.value) { @@ -348,7 +348,7 @@ export default function Thread() { notes={replies} related={getAllReactions( thread.data, - replies.map(a => a.id) + replies.map(a => a.id), )} chains={chains} onNavigate={navigateThread} diff --git a/packages/app/src/Element/Timeline.tsx b/packages/app/src/Element/Timeline.tsx index acdd12217..0b5c545a4 100644 --- a/packages/app/src/Element/Timeline.tsx +++ b/packages/app/src/Element/Timeline.tsx @@ -47,7 +47,7 @@ const Timeline = (props: TimelineProps) => { ?.filter(a => (props.postsOnly ? !a.tags.some(b => b[0] === "e") : true)) .filter(a => props.ignoreModeration || !isMuted(a.pubkey)); }, - [props.postsOnly, muted, props.ignoreModeration] + [props.postsOnly, muted, props.ignoreModeration], ); const mainFeed = useMemo(() => { @@ -60,7 +60,7 @@ const Timeline = (props: TimelineProps) => { (id: u256) => { return (feed.related ?? []).filter(a => findTag(a, "e") === id); }, - [feed.related] + [feed.related], ); const liveStreams = useMemo(() => { return (feed.main ?? []).filter(a => a.kind === EventKind.LiveEvent && findTag(a, "status") === "live"); diff --git a/packages/app/src/Element/TimelineFollows.tsx b/packages/app/src/Element/TimelineFollows.tsx index 8866a2857..c96499683 100644 --- a/packages/app/src/Element/TimelineFollows.tsx +++ b/packages/app/src/Element/TimelineFollows.tsx @@ -28,11 +28,11 @@ const TimelineFollows = (props: TimelineFollowsProps) => { const [latest, setLatest] = useState(unixNow()); const feed = useSyncExternalStore( cb => FollowsFeed.hook(cb, "*"), - () => FollowsFeed.snapshot() + () => FollowsFeed.snapshot(), ); const reactions = useReactions( "follows-feed-reactions", - feed.map(a => a.id) + feed.map(a => a.id), ); const system = useContext(SnortContext); const login = useLogin(); @@ -48,7 +48,7 @@ const TimelineFollows = (props: TimelineFollowsProps) => { ?.filter(a => (props.postsOnly ? !a.tags.some(b => b[0] === "e") : true)) .filter(a => !isMuted(a.pubkey) && login.follows.item.includes(a.pubkey)); }, - [props.postsOnly, muted, login.follows.timestamp] + [props.postsOnly, muted, login.follows.timestamp], ); const mainFeed = useMemo(() => { @@ -63,7 +63,7 @@ const TimelineFollows = (props: TimelineFollowsProps) => { (id: u256) => { return (reactions?.data ?? []).filter(a => findTag(a, "e") === id); }, - [reactions] + [reactions], ); const liveStreams = useMemo(() => { diff --git a/packages/app/src/Element/ZapstrEmbed.tsx b/packages/app/src/Element/ZapstrEmbed.tsx index 844b5b67c..fa112e8a7 100644 --- a/packages/app/src/Element/ZapstrEmbed.tsx +++ b/packages/app/src/Element/ZapstrEmbed.tsx @@ -17,7 +17,7 @@ export default function ZapstrEmbed({ ev }: { ev: NostrEvent }) { ev.tags.find(a => a[0] === "d")?.[1] ?? "", undefined, ev.kind, - ev.pubkey + ev.pubkey, ); return ( <> diff --git a/packages/app/src/Feed/BadgesFeed.ts b/packages/app/src/Feed/BadgesFeed.ts index d6905c5d0..d180f160c 100644 --- a/packages/app/src/Feed/BadgesFeed.ts +++ b/packages/app/src/Feed/BadgesFeed.ts @@ -23,7 +23,7 @@ export default function useProfileBadges(pubkey?: HexKey) { if (profileBadges.data) { return chunks( profileBadges.data.tags.filter(t => t[0] === "a" || t[0] === "e"), - 2 + 2, ).reduce((acc, [a, e]) => { return { ...acc, @@ -44,7 +44,7 @@ export default function useProfileBadges(pubkey?: HexKey) { } return acc; }, - { pubkeys: [], ds: [] } as BadgeAwards + { pubkeys: [], ds: [] } as BadgeAwards, ) as BadgeAwards; }, [profile]); @@ -77,7 +77,7 @@ export default function useProfileBadges(pubkey?: HexKey) { }) .filter( ({ award, badge }) => - badge && award.pubkey === badge.pubkey && award.tags.find(t => t[0] === "p" && t[1] === pubkey) + badge && award.pubkey === badge.pubkey && award.tags.find(t => t[0] === "p" && t[1] === pubkey), ) .map(({ badge }) => unwrap(badge)); } diff --git a/packages/app/src/Feed/FeedReactions.ts b/packages/app/src/Feed/FeedReactions.ts index cd32b57aa..5faaeb004 100644 --- a/packages/app/src/Feed/FeedReactions.ts +++ b/packages/app/src/Feed/FeedReactions.ts @@ -13,7 +13,7 @@ export function useReactions(subId: string, ids: Array, others?: (rb: Re .kinds( pref.enableReactions ? [EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt] - : [EventKind.ZapReceipt, EventKind.Repost] + : [EventKind.ZapReceipt, EventKind.Repost], ) .tag("e", ids); } diff --git a/packages/app/src/Feed/FollowersFeed.ts b/packages/app/src/Feed/FollowersFeed.ts index 84544a292..e8b5c2898 100644 --- a/packages/app/src/Feed/FollowersFeed.ts +++ b/packages/app/src/Feed/FollowersFeed.ts @@ -14,7 +14,7 @@ export default function useFollowersFeed(pubkey?: HexKey) { const followers = useMemo(() => { const contactLists = followersFeed.data?.filter( - a => a.kind === EventKind.ContactList && a.tags.some(b => b[0] === "p" && b[1] === pubkey) + a => a.kind === EventKind.ContactList && a.tags.some(b => b[0] === "p" && b[1] === pubkey), ); return [...new Set(contactLists?.map(a => a.pubkey))]; }, [followersFeed, pubkey]); diff --git a/packages/app/src/Feed/LoginFeed.ts b/packages/app/src/Feed/LoginFeed.ts index 8eceb79fe..f1eda0d6a 100644 --- a/packages/app/src/Feed/LoginFeed.ts +++ b/packages/app/src/Feed/LoginFeed.ts @@ -85,7 +85,7 @@ export default function useLoginFeed() { Nip4Chats.onEvent(loginFeed.data); const subs = loginFeed.data.filter( - a => a.kind === EventKind.SnortSubscriptions && a.pubkey === bech32ToHex(SnortPubKey) + a => a.kind === EventKind.SnortSubscriptions && a.pubkey === bech32ToHex(SnortPubKey), ); Promise.all( subs.map(async a => { @@ -97,7 +97,7 @@ export default function useLoginFeed() { ...ex, } as SubscriptionEvent; } - }) + }), ).then(a => addSubscription(login, ...a.filter(a => a !== undefined).map(unwrap))); } }, [loginFeed, publisher]); @@ -106,7 +106,7 @@ export default function useLoginFeed() { useEffect(() => { if (loginFeed.data) { const replies = loginFeed.data.filter( - a => a.kind === EventKind.TextNote && !isMuted(a.pubkey) && a.created_at > readNotifications + a => a.kind === EventKind.TextNote && !isMuted(a.pubkey) && a.created_at > readNotifications, ); replies.forEach(async nx => { const n = await makeNotification(nx); diff --git a/packages/app/src/Feed/ThreadFeed.ts b/packages/app/src/Feed/ThreadFeed.ts index 6603bd2d3..8fe289a2b 100644 --- a/packages/app/src/Feed/ThreadFeed.ts +++ b/packages/app/src/Feed/ThreadFeed.ts @@ -35,11 +35,11 @@ export default function useThreadFeed(link: NostrLink) { .kinds( pref.enableReactions ? [EventKind.Reaction, EventKind.TextNote, EventKind.Repost, EventKind.ZapReceipt] - : [EventKind.TextNote, EventKind.ZapReceipt, EventKind.Repost] + : [EventKind.TextNote, EventKind.ZapReceipt, EventKind.Repost], ) .tag( "e", - allEvents.map(a => a.id) + allEvents.map(a => a.id), ); } if (trackingATags.length > 0) { @@ -50,7 +50,7 @@ export default function useThreadFeed(link: NostrLink) { .authors(parsed.map(a => a[1])) .tag( "d", - parsed.map(a => a[2]) + parsed.map(a => a[2]), ); sub.withFilter().tag("a", trackingATags); } @@ -85,7 +85,7 @@ export default function useThreadFeed(link: NostrLink) { id: b[1], relay: b[2], }; - }) + }), ) .flat(); const eTagsMissing = eTags.filter(a => !mainNotes.some(b => b.id === a.id)); diff --git a/packages/app/src/Feed/TimelineFeed.ts b/packages/app/src/Feed/TimelineFeed.ts index ea45ede4d..6feb29da4 100644 --- a/packages/app/src/Feed/TimelineFeed.ts +++ b/packages/app/src/Feed/TimelineFeed.ts @@ -43,7 +43,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel .kinds( subject.type === "profile_keyword" ? [EventKind.SetMetadata] - : [EventKind.TextNote, EventKind.Repost, EventKind.Polls] + : [EventKind.TextNote, EventKind.Repost, EventKind.Polls], ); if (subject.relay) { @@ -149,7 +149,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel .map(a => unwrap(a)[1]); const repostsByKind1 = main.data .filter( - a => (a.kind === EventKind.Repost || a.kind === EventKind.TextNote) && a.tags.some(tagFilterOfTextRepost(a)) + a => (a.kind === EventKind.Repost || a.kind === EventKind.TextNote) && a.tags.some(tagFilterOfTextRepost(a)), ) .map(a => a.tags.find(tagFilterOfTextRepost(a))) .filter(a => a) diff --git a/packages/app/src/Hooks/useImgProxy.ts b/packages/app/src/Hooks/useImgProxy.ts index 090fa4f4c..2f1a04cd3 100644 --- a/packages/app/src/Hooks/useImgProxy.ts +++ b/packages/app/src/Hooks/useImgProxy.ts @@ -21,7 +21,7 @@ export default function useImgProxy() { const result = hmacSha256( utils.hexToBytes(unwrap(settings).key), utils.hexToBytes(unwrap(settings).salt), - te.encode(u) + te.encode(u), ); return urlSafe(base64.encode(result)); } diff --git a/packages/app/src/Hooks/useInteractionCache.tsx b/packages/app/src/Hooks/useInteractionCache.tsx index ab73a99cd..64cc81228 100644 --- a/packages/app/src/Hooks/useInteractionCache.tsx +++ b/packages/app/src/Hooks/useInteractionCache.tsx @@ -15,7 +15,7 @@ export function useInteractionCache(pubkey?: HexKey, event?: u256) { const data = useSyncExternalStore( c => InteractionCache.hook(c, id), - () => InteractionCache.snapshot().find(a => a.id === id) + () => InteractionCache.snapshot().find(a => a.id === id), ) || EmptyInteraction; return { data: data, diff --git a/packages/app/src/Hooks/useLogin.tsx b/packages/app/src/Hooks/useLogin.tsx index b4ffedffd..cf3a205e5 100644 --- a/packages/app/src/Hooks/useLogin.tsx +++ b/packages/app/src/Hooks/useLogin.tsx @@ -4,6 +4,6 @@ import { useSyncExternalStore } from "react"; export default function useLogin() { return useSyncExternalStore( s => LoginStore.hook(s), - () => LoginStore.snapshot() + () => LoginStore.snapshot(), ); } diff --git a/packages/app/src/Login/MultiAccountStore.ts b/packages/app/src/Login/MultiAccountStore.ts index 3c35ec226..1b25a66c9 100644 --- a/packages/app/src/Login/MultiAccountStore.ts +++ b/packages/app/src/Login/MultiAccountStore.ts @@ -96,7 +96,7 @@ export class MultiAccountStore extends ExternalStore { type: LoginSessionType, relays?: Record, remoteSignerRelays?: Array, - privateKey?: string + privateKey?: string, ) { if (this.#accounts.has(key)) { throw new Error("Already logged in with this pubkey"); diff --git a/packages/app/src/Nip05/ServiceProvider.ts b/packages/app/src/Nip05/ServiceProvider.ts index 610d4ed53..eda713714 100644 --- a/packages/app/src/Nip05/ServiceProvider.ts +++ b/packages/app/src/Nip05/ServiceProvider.ts @@ -97,7 +97,7 @@ export class ServiceProvider { path: string, method?: "GET" | string, body?: unknown, - headers?: { [key: string]: string } + headers?: { [key: string]: string }, ): Promise { try { const rsp = await fetch(`${this.url}${path}`, { diff --git a/packages/app/src/Nip05/SnortServiceProvider.ts b/packages/app/src/Nip05/SnortServiceProvider.ts index d6efa314a..f4de626a2 100644 --- a/packages/app/src/Nip05/SnortServiceProvider.ts +++ b/packages/app/src/Nip05/SnortServiceProvider.ts @@ -54,7 +54,7 @@ export default class SnortServiceProvider extends ServiceProvider { path: string, method?: "GET" | string, body?: unknown, - headers?: { [key: string]: string } + headers?: { [key: string]: string }, ): Promise { const auth = await this.#publisher.generic(eb => { eb.kind(EventKind.HttpAuthentication); diff --git a/packages/app/src/Pages/ErrorPage.tsx b/packages/app/src/Pages/ErrorPage.tsx index 687c068d5..b3fcb5410 100644 --- a/packages/app/src/Pages/ErrorPage.tsx +++ b/packages/app/src/Pages/ErrorPage.tsx @@ -25,7 +25,7 @@ const ErrorPage = () => { {JSON.stringify( error instanceof Error ? { name: error.name, message: error.message, stack: error.stack } : error, undefined, - " " + " ", )} } diff --git a/packages/app/src/Pages/Layout.tsx b/packages/app/src/Pages/Layout.tsx index 1df8c6048..a428f9410 100644 --- a/packages/app/src/Pages/Layout.tsx +++ b/packages/app/src/Pages/Layout.tsx @@ -89,7 +89,7 @@ export default function Layout() { useEffect(() => { const osTheme = window.matchMedia("(prefers-color-scheme: light)"); setTheme( - preferences.theme === "system" && osTheme.matches ? "light" : preferences.theme === "light" ? "light" : "dark" + preferences.theme === "system" && osTheme.matches ? "light" : preferences.theme === "light" ? "light" : "dark", ); osTheme.onchange = e => { @@ -174,7 +174,7 @@ const AccountHeader = () => { const hasNotifications = useMemo( () => latestNotification > readNotifications, - [latestNotification, readNotifications] + [latestNotification, readNotifications], ); const unreadDms = useMemo(() => (publicKey ? 0 : 0), [publicKey]); diff --git a/packages/app/src/Pages/LoginPage.tsx b/packages/app/src/Pages/LoginPage.tsx index ba004d7b1..07401626e 100644 --- a/packages/app/src/Pages/LoginPage.tsx +++ b/packages/app/src/Pages/LoginPage.tsx @@ -55,7 +55,7 @@ const Artwork: Array = [ export async function getNip05PubKey(addr: string): Promise { const [username, domain] = addr.split("@"); const rsp = await fetch( - `https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(username.toLocaleLowerCase())}` + `https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(username.toLocaleLowerCase())}`, ); if (rsp.ok) { const data = await rsp.json(); @@ -103,7 +103,7 @@ export default function LoginPage() { setError( formatMessage({ defaultMessage: "Unknown login error", - }) + }), ); } console.error(e); diff --git a/packages/app/src/Pages/Notifications.tsx b/packages/app/src/Pages/Notifications.tsx index 7af560251..23cefcd7b 100644 --- a/packages/app/src/Pages/Notifications.tsx +++ b/packages/app/src/Pages/Notifications.tsx @@ -75,7 +75,7 @@ export default function NotificationsPage() { const notifications = useSyncExternalStore( c => Notifications.hook(c, "*"), - () => Notifications.snapshot() + () => Notifications.snapshot(), ); const timeKey = (ev: NostrEvent) => { @@ -119,7 +119,7 @@ function NotificationGroup({ evs }: { evs: Array }) { return zap.anonZap ? "anon" : zap.sender ?? a.pubkey; } return a.pubkey; - }) + }), ); const firstPubkey = pubkeys[0]; const firstPubkeyProfile = useUserProfile(inView ? (firstPubkey === "anon" ? "" : firstPubkey) : ""); @@ -213,7 +213,7 @@ function NotificationGroup({ evs }: { evs: Array }) { pubkeys.length - 1, firstPubkey === "anon" ? formatMessage({ defaultMessage: "Anon" }) - : getDisplayName(firstPubkeyProfile, firstPubkey) + : getDisplayName(firstPubkeyProfile, firstPubkey), )} )} diff --git a/packages/app/src/Pages/ProfilePage.tsx b/packages/app/src/Pages/ProfilePage.tsx index 434b816bf..b5655eec1 100644 --- a/packages/app/src/Pages/ProfilePage.tsx +++ b/packages/app/src/Pages/ProfilePage.tsx @@ -221,7 +221,7 @@ export default function ProfilePage() { } as { [key: string]: Tab }; const [tab, setTab] = useState(ProfileTab.Notes); const optionalTabs = [ProfileTab.Zaps, ProfileTab.Relays, ProfileTab.Bookmarks, ProfileTab.Muted].filter(a => - unwrap(a) + unwrap(a), ) as Tab[]; const horizontalScroll = useHorizontalScroll(); @@ -433,7 +433,7 @@ export default function ProfilePage() { type: TLVEntryType.Author, length: 64, value: id, - })}` + })}`, ) }> diff --git a/packages/app/src/Pages/ZapPool.tsx b/packages/app/src/Pages/ZapPool.tsx index f6703ee3d..26664371e 100644 --- a/packages/app/src/Pages/ZapPool.tsx +++ b/packages/app/src/Pages/ZapPool.tsx @@ -73,7 +73,7 @@ export default function ZapPoolPage() { const login = useLogin(); const zapPool = useSyncExternalStore( c => ZapPoolController.hook(c), - () => ZapPoolController.snapshot() + () => ZapPoolController.snapshot(), ); const { wallet } = useWallet(); diff --git a/packages/app/src/Pages/settings/Accounts.tsx b/packages/app/src/Pages/settings/Accounts.tsx index 9ad666963..ce113010b 100644 --- a/packages/app/src/Pages/settings/Accounts.tsx +++ b/packages/app/src/Pages/settings/Accounts.tsx @@ -27,7 +27,7 @@ export default function AccountsPage() { setError( formatMessage({ defaultMessage: "Unknown login error", - }) + }), ); } console.error(e); diff --git a/packages/app/src/Pages/settings/handle/LNAddress.tsx b/packages/app/src/Pages/settings/handle/LNAddress.tsx index 562dc571e..0cad952a3 100644 --- a/packages/app/src/Pages/settings/handle/LNAddress.tsx +++ b/packages/app/src/Pages/settings/handle/LNAddress.tsx @@ -29,7 +29,7 @@ export default function LNForwardAddress({ handle }: { handle: ManageHandle }) { setError( formatMessage({ defaultMessage: "Invalid LNURL", - }) + }), ); return; } diff --git a/packages/app/src/Pages/settings/wallet/Cashu.tsx b/packages/app/src/Pages/settings/wallet/Cashu.tsx index 5a2f4efa2..e1a6d4117 100644 --- a/packages/app/src/Pages/settings/wallet/Cashu.tsx +++ b/packages/app/src/Pages/settings/wallet/Cashu.tsx @@ -39,7 +39,7 @@ const ConnectCashu = () => { setError( formatMessage({ defaultMessage: "Unknown error", - }) + }), ); } } diff --git a/packages/app/src/Pages/settings/wallet/LNC.tsx b/packages/app/src/Pages/settings/wallet/LNC.tsx index d33fa9fe1..64c66d212 100644 --- a/packages/app/src/Pages/settings/wallet/LNC.tsx +++ b/packages/app/src/Pages/settings/wallet/LNC.tsx @@ -32,7 +32,7 @@ const ConnectLNC = () => { setError( formatMessage({ defaultMessage: "Unknown error", - }) + }), ); } } diff --git a/packages/app/src/Pages/settings/wallet/LNDHub.tsx b/packages/app/src/Pages/settings/wallet/LNDHub.tsx index b33462ec0..d2c20f446 100644 --- a/packages/app/src/Pages/settings/wallet/LNDHub.tsx +++ b/packages/app/src/Pages/settings/wallet/LNDHub.tsx @@ -37,7 +37,7 @@ const ConnectLNDHub = () => { setError( formatMessage({ defaultMessage: "Unknown error", - }) + }), ); } } diff --git a/packages/app/src/Pages/settings/wallet/NWC.tsx b/packages/app/src/Pages/settings/wallet/NWC.tsx index 921d6c598..41419615f 100644 --- a/packages/app/src/Pages/settings/wallet/NWC.tsx +++ b/packages/app/src/Pages/settings/wallet/NWC.tsx @@ -37,7 +37,7 @@ const ConnectNostrWallet = () => { setError( formatMessage({ defaultMessage: "Unknown error", - }) + }), ); } } diff --git a/packages/app/src/SnortApi.ts b/packages/app/src/SnortApi.ts index a69f71e67..f29a529e7 100644 --- a/packages/app/src/SnortApi.ts +++ b/packages/app/src/SnortApi.ts @@ -88,7 +88,7 @@ export default class SnortApi { path: string, method?: "GET" | string, body?: { [key: string]: string }, - headers?: { [key: string]: string } + headers?: { [key: string]: string }, ): Promise { if (!this.#publisher) { throw new Error("Publisher not set"); @@ -110,7 +110,7 @@ export default class SnortApi { path: string, method?: "GET" | string, body?: { [key: string]: string }, - headers?: { [key: string]: string } + headers?: { [key: string]: string }, ): Promise { const rsp = await fetch(`${this.#url}${path}`, { method: method, diff --git a/packages/app/src/SnortUtils/index.ts b/packages/app/src/SnortUtils/index.ts index 242b516fc..65062b2a5 100644 --- a/packages/app/src/SnortUtils/index.ts +++ b/packages/app/src/SnortUtils/index.ts @@ -50,7 +50,7 @@ export async function openFile(): Promise { } }, 300); }, - { once: true } + { once: true }, ); }); } @@ -209,7 +209,7 @@ export function dedupeByPubkey(events: TaggedNostrEvent[]) { list: [...list, ev], }; }, - { list: [], seen: new Set([]) } + { list: [], seen: new Set([]) }, ); return deduped.list as TaggedNostrEvent[]; } @@ -226,7 +226,7 @@ export function dedupeById(events: Array) { list: [...list, ev], }; }, - { list: [], seen: new Set([]) } + { list: [], seen: new Set([]) }, ); return deduped.list as Array; } @@ -487,7 +487,7 @@ export function kvToObject(o: string, sep?: string) { return [match[1], match[2]]; } return []; - }) + }), ) as T; } diff --git a/packages/app/src/Toaster.tsx b/packages/app/src/Toaster.tsx index eecb85ca2..1d45a177c 100644 --- a/packages/app/src/Toaster.tsx +++ b/packages/app/src/Toaster.tsx @@ -39,7 +39,7 @@ export const Toastore = new ToasterSlots(); export default function Toaster() { const toast = useSyncExternalStore( c => Toastore.hook(c), - () => Toastore.snapshot() + () => Toastore.snapshot(), ); return ( diff --git a/packages/app/src/Upload/VoidCat.ts b/packages/app/src/Upload/VoidCat.ts index 8eb8a11a4..0fe5e010a 100644 --- a/packages/app/src/Upload/VoidCat.ts +++ b/packages/app/src/Upload/VoidCat.ts @@ -12,7 +12,7 @@ import { magnetURIDecode } from "SnortUtils"; export default async function VoidCatUpload( file: File | Blob, filename: string, - publisher?: EventPublisher + publisher?: EventPublisher, ): Promise { const api = new VoidApi(VoidCatHost); const uploader = api.getUploader(file); diff --git a/packages/app/src/Wallet/Cashu.ts b/packages/app/src/Wallet/Cashu.ts index 541e65ba1..b88dc13ce 100644 --- a/packages/app/src/Wallet/Cashu.ts +++ b/packages/app/src/Wallet/Cashu.ts @@ -57,6 +57,6 @@ export interface NutStashBackup { mints: [ { mintURL: string; - } + }, ]; } diff --git a/packages/app/src/Wallet/LNCWallet.ts b/packages/app/src/Wallet/LNCWallet.ts index 342dfec86..6093289ed 100644 --- a/packages/app/src/Wallet/LNCWallet.ts +++ b/packages/app/src/Wallet/LNCWallet.ts @@ -126,7 +126,7 @@ export class LNCWallet implements LNWallet { err => { this.#log(err); reject(err); - } + }, ); }); } diff --git a/packages/app/src/Wallet/NostrWalletConnect.ts b/packages/app/src/Wallet/NostrWalletConnect.ts index d2d1113d2..5930e60d2 100644 --- a/packages/app/src/Wallet/NostrWalletConnect.ts +++ b/packages/app/src/Wallet/NostrWalletConnect.ts @@ -182,7 +182,7 @@ export class NostrConnectWallet implements LNWallet { ], () => { // ignored - } + }, ); await this.#conn.SendAsync(evCommand); return await new Promise((resolve, reject) => { diff --git a/packages/app/src/Wallet/WebLN.ts b/packages/app/src/Wallet/WebLN.ts index e97082772..68db42121 100644 --- a/packages/app/src/Wallet/WebLN.ts +++ b/packages/app/src/Wallet/WebLN.ts @@ -84,7 +84,7 @@ export class WebLNWallet implements LNWallet { await window.webln?.makeInvoice({ amount: req.amount, defaultMemo: req.memo, - }) + }), ); if (rsp) { const invoice = prToWalletInvoice(rsp.paymentRequest); diff --git a/packages/app/src/Wallet/index.ts b/packages/app/src/Wallet/index.ts index ad8a01dd1..cb5b98e0c 100644 --- a/packages/app/src/Wallet/index.ts +++ b/packages/app/src/Wallet/index.ts @@ -248,7 +248,7 @@ window.document.addEventListener("close", () => { export function useWallet() { const wallet = useSyncExternalStore( h => Wallets.hook(h), - () => Wallets.snapshot() + () => Wallets.snapshot(), ); useEffect(() => { if (wallet.wallet?.isReady() === false && wallet.wallet.canAutoLogin()) { diff --git a/packages/app/src/ZapPoolController.ts b/packages/app/src/ZapPoolController.ts index 0695b5d7d..617933bc8 100644 --- a/packages/app/src/ZapPoolController.ts +++ b/packages/app/src/ZapPoolController.ts @@ -54,7 +54,7 @@ class ZapPool extends ExternalStore> { Toastore.push({ element: `Sent ${amtSend.toLocaleString()} sats to ${getDisplayName( profile, - x.pubkey + x.pubkey, )} from your zap pool`, expire: unixNow() + 10, icon: "zap", diff --git a/packages/app/src/benchmarks.ts b/packages/app/src/benchmarks.ts new file mode 100644 index 000000000..1b6acb80f --- /dev/null +++ b/packages/app/src/benchmarks.ts @@ -0,0 +1,114 @@ +import { bytesToHex } from "@noble/hashes/utils"; +import { DefaultQueryOptimizer, FlatReqFilter, QueryOptimizer, ReqFilter } from "@snort/system"; +import { compress, expand_filter, flat_merge, get_diff, default as wasmInit } from "@snort/system-query"; +import WasmPath from "@snort/system-query/pkg/system_query_bg.wasm"; +import { Bench } from "tinybench"; + +const WasmQueryOptimizer = { + expandFilter: (f: ReqFilter) => { + return expand_filter(f) as Array; + }, + getDiff: (prev: Array, next: Array) => { + return get_diff(prev, next) as Array; + }, + flatMerge: (all: Array) => { + return flat_merge(all) as Array; + }, + compress: (all: Array) => { + return compress(all) as Array; + }, +} as QueryOptimizer; + +const makeOnePubkey = () => { + const rnd = globalThis.crypto.getRandomValues(new Uint8Array(32)); + return bytesToHex(rnd); +}; + +const randomPubkeys = (() => { + const ret = []; + for (let x = 0; x < 50; x++) { + ret.push(makeOnePubkey()); + } + return ret; +})(); + +const testExpand = (q: QueryOptimizer) => { + q.expandFilter({ + kinds: [1, 2, 3], + authors: randomPubkeys, + }); +}; +const testGetDiff = (q: QueryOptimizer) => { + q.getDiff( + [ + { + kinds: [1, 2, 3], + authors: randomPubkeys, + }, + ], + [ + { + kinds: [1, 2, 3, 4, 5], + authors: randomPubkeys, + }, + ], + ); +}; +const testFlatMerge = (q: QueryOptimizer) => { + q.flatMerge( + q.expandFilter({ + kinds: [1, 6, 7, 6969], + authors: randomPubkeys, + }), + ); +}; +const testCompress = (q: QueryOptimizer) => { + q.compress([ + { + kinds: [1, 6, 7, 6969], + authors: randomPubkeys, + }, + { + kinds: [1, 6, 7, 6969], + authors: randomPubkeys, + }, + { + kinds: [1, 6, 7, 6969], + authors: randomPubkeys, + }, + { + kinds: [1, 6, 7, 6969], + authors: randomPubkeys, + }, + { + kinds: [1, 6, 7, 6969], + authors: randomPubkeys, + }, + ]); +}; + +const wasmSuite = new Bench({ time: 1_000 }); +const suite = new Bench({ time: 1_000 }); + +const addTests = (s: Bench, q: QueryOptimizer) => { + s.add("expand", () => testExpand(q)); + s.add("get_diff", () => testGetDiff(q)); + s.add("flat_merge", () => testFlatMerge(q)); + s.add("compress", () => testCompress(q)); +}; + +addTests(suite, DefaultQueryOptimizer); +addTests(wasmSuite, WasmQueryOptimizer); + +const runAll = async () => { + await wasmInit(WasmPath); + + console.log("DefaultQueryOptimizer"); + await suite.run(); + console.table(suite.table()); + + console.log("WasmQueryOptimizer"); + await wasmSuite.run(); + console.table(wasmSuite.table()); +}; +runAll().catch(console.error); diff --git a/packages/app/src/chat/index.ts b/packages/app/src/chat/index.ts index f29f9122d..d6b9326ae 100644 --- a/packages/app/src/chat/index.ts +++ b/packages/app/src/chat/index.ts @@ -116,7 +116,7 @@ export function createChatLink(type: ChatType, ...params: Array) { type: TLVEntryType.Author, length: params[0].length, value: params[0], - } as TLVEntry + } as TLVEntry, )}`; } case ChatType.PrivateDirectMessage: { @@ -127,7 +127,7 @@ export function createChatLink(type: ChatType, ...params: Array) { type: TLVEntryType.Author, length: params[0].length, value: params[0], - } as TLVEntry + } as TLVEntry, )}`; } case ChatType.PrivateGroupChat: { @@ -139,8 +139,8 @@ export function createChatLink(type: ChatType, ...params: Array) { type: TLVEntryType.Author, length: a.length, value: a, - } as TLVEntry) - ) + }) as TLVEntry, + ), )}`; } } @@ -161,14 +161,14 @@ export function useNip4Chat() { const { publicKey } = useLogin(); return useSyncExternalStore( c => Nip4Chats.hook(c), - () => Nip4Chats.snapshot(publicKey) + () => Nip4Chats.snapshot(publicKey), ); } export function useNip29Chat() { return useSyncExternalStore( c => Nip29Chats.hook(c), - () => Nip29Chats.snapshot() + () => Nip29Chats.snapshot(), ); } @@ -176,7 +176,7 @@ export function useNip24Chat() { const { publicKey } = useLogin(); return useSyncExternalStore( c => Nip24Chats.hook(c), - () => Nip24Chats.snapshot(publicKey) + () => Nip24Chats.snapshot(publicKey), ); } diff --git a/packages/app/src/chat/nip24.ts b/packages/app/src/chat/nip24.ts index 9344e6fbc..6281a6b79 100644 --- a/packages/app/src/chat/nip24.ts +++ b/packages/app/src/chat/nip24.ts @@ -39,8 +39,8 @@ export class Nip24ChatSystem extends ExternalStore> implements ChatS value: v, type: TLVEntryType.Author, length: v.length, - } as TLVEntry) - ) + }) as TLVEntry, + ), ); }; return dedupe(messages.map(a => chatId(a))).map(a => { @@ -72,7 +72,7 @@ export class Nip24ChatSystem extends ExternalStore> implements ChatS } as { t: number; title: string | undefined; - } + }, ); return { type: ChatType.PrivateDirectMessage, diff --git a/packages/app/src/chat/nip29.ts b/packages/app/src/chat/nip29.ts index 956472e79..261f7e86d 100644 --- a/packages/app/src/chat/nip29.ts +++ b/packages/app/src/chat/nip29.ts @@ -46,12 +46,12 @@ export class Nip29ChatSystem extends ExternalStore> implements ChatS .map(a => a.tags.find(b => b[0] === "g")) .filter(a => a !== undefined) .map(a => unwrap(a)) - .map(a => `${a[2]}${a[1]}`) + .map(a => `${a[2]}${a[1]}`), ); return groups.map(g => { const [relay, channel] = g.split("/", 2); const messages = allMessages.filter( - a => `${a.tags.find(b => b[0] === "g")?.[2]}${a.tags.find(b => b[0] === "g")?.[1]}` === g + a => `${a.tags.find(b => b[0] === "g")?.[2]}${a.tags.find(b => b[0] === "g")?.[1]}` === g, ); const lastRead = lastReadInChat(g); return { diff --git a/packages/app/src/chat/nip4.ts b/packages/app/src/chat/nip4.ts index c766ba5a8..f011d5757 100644 --- a/packages/app/src/chat/nip4.ts +++ b/packages/app/src/chat/nip4.ts @@ -34,7 +34,7 @@ export class Nip4ChatSystem extends ExternalStore> implements ChatSy const dms = this.#cache.snapshot(); const dmSince = dms.reduce( (acc, v) => (v.created_at > acc && v.kind === EventKind.DirectMessage ? (acc = v.created_at) : acc), - 0 + 0, ); this.#log("Loading DMS since %s", new Date(dmSince * 1000)); @@ -49,12 +49,15 @@ export class Nip4ChatSystem extends ExternalStore> implements ChatSy listChats(pk: string): Chat[] { const myDms = this.#nip4Events(); - const chats = myDms.reduce((acc, v) => { - const chatId = inChatWith(v, pk); - acc[chatId] ??= []; - acc[chatId].push(v); - return acc; - }, {} as Record>); + const chats = myDms.reduce( + (acc, v) => { + const chatId = inChatWith(v, pk); + acc[chatId] ??= []; + acc[chatId].push(v); + return acc; + }, + {} as Record>, + ); return [...Object.entries(chats)].map(([k, v]) => Nip4ChatSystem.createChatObj(k, v)); } diff --git a/packages/app/src/index.tsx b/packages/app/src/index.tsx index 7666afb05..b86f360c5 100644 --- a/packages/app/src/index.tsx +++ b/packages/app/src/index.tsx @@ -2,11 +2,23 @@ import "./index.css"; import "@szhsin/react-menu/dist/index.css"; import "./fonts/inter.css"; +import { compress, expand_filter, flat_merge, get_diff, default as wasmInit } from "@snort/system-query"; +import WasmPath from "@snort/system-query/pkg/system_query_bg.wasm"; + import { StrictMode } from "react"; import * as ReactDOM from "react-dom/client"; import { Provider } from "react-redux"; import { createBrowserRouter, RouterProvider } from "react-router-dom"; -import { EventPublisher, NostrSystem, ProfileLoaderService, Nip7Signer, PowWorker } from "@snort/system"; +import { + EventPublisher, + NostrSystem, + ProfileLoaderService, + Nip7Signer, + PowWorker, + QueryOptimizer, + FlatReqFilter, + ReqFilter, +} from "@snort/system"; import { SnortContext } from "@snort/system-react"; import * as serviceWorkerRegistration from "serviceWorkerRegistration"; @@ -36,6 +48,21 @@ import { db } from "Db"; import { preload, RelayMetrics, UserCache, UserRelays } from "Cache"; import { LoginStore } from "Login"; +const WasmQueryOptimizer = { + expandFilter: (f: ReqFilter) => { + return expand_filter(f) as Array; + }, + getDiff: (prev: Array, next: Array) => { + return get_diff(prev, next) as Array; + }, + flatMerge: (all: Array) => { + return flat_merge(all) as Array; + }, + compress: (all: Array) => { + return compress(all) as Array; + }, +} as QueryOptimizer; + /** * Singleton nostr system */ @@ -43,6 +70,7 @@ export const System = new NostrSystem({ relayCache: UserRelays, profileCache: UserCache, relayMetrics: RelayMetrics, + queryOptimizer: WasmQueryOptimizer, authHandler: async (c, r) => { const { publicKey, privateKey } = LoginStore.snapshot(); if (privateKey) { @@ -69,6 +97,7 @@ export const DefaultPowWorker = new PowWorker("/pow.js"); serviceWorkerRegistration.register(); async function initSite() { + await wasmInit(WasmPath); const login = LoginStore.takeSnapshot(); db.ready = await db.isAvailable(); if (db.ready) { @@ -173,5 +202,5 @@ root.render( - + , ); diff --git a/packages/app/webpack.config.js b/packages/app/webpack.config.js index 1708f242c..d002edba3 100644 --- a/packages/app/webpack.config.js +++ b/packages/app/webpack.config.js @@ -19,6 +19,7 @@ const config = { import: require.resolve("@snort/system/dist/pow-worker.js"), filename: "pow.js", }, + bench: "./src/benchmarks.ts", }, target: "browserslist", mode: isProduction ? "production" : "development", @@ -47,7 +48,12 @@ const config = { new HtmlWebpackPlugin({ template: "public/index.html", favicon: "public/favicon.ico", - excludeChunks: ["pow"], + excludeChunks: ["pow", "bench"], + }), + new HtmlWebpackPlugin({ + filename: "bench.html", + template: "public/bench.html", + chunks: ["bench"], }), new ESLintPlugin({ extensions: ["js", "mjs", "jsx", "ts", "tsx"], @@ -115,7 +121,7 @@ const config = { use: [MiniCssExtractPlugin.loader, require.resolve("css-loader")], }, { - test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif|webp)$/i, + test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif|webp|wasm)$/i, type: "asset", }, ], diff --git a/packages/system-query/.gitignore b/packages/system-query/.gitignore new file mode 100644 index 000000000..d6c6ec7e2 --- /dev/null +++ b/packages/system-query/.gitignore @@ -0,0 +1,3 @@ +.idea/ +target/ +*.txt \ No newline at end of file diff --git a/packages/system-query/Cargo.lock b/packages/system-query/Cargo.lock new file mode 100644 index 000000000..067a7bddf --- /dev/null +++ b/packages/system-query/Cargo.lock @@ -0,0 +1,831 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" +dependencies = [ + "memchr", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a13b88d2c62ff462f88e4a121f17a82c1af05693a2f192b5c38d14de73c19f6" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "errno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "linux-raw-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + +[[package]] +name = "plotters" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" + +[[package]] +name = "plotters-svg" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "regex" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "rustix" +version = "0.38.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_derive" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "2.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "system-query" +version = "0.1.0" +dependencies = [ + "criterion", + "itertools 0.11.0", + "rand", + "serde", + "serde-wasm-bindgen", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-test", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e6e302a7ea94f83a6d09e78e7dc7d9ca7b186bc2829c24a22d0753efd680671" +dependencies = [ + "console_error_panic_hook", + "js-sys", + "scoped-tls", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecb993dd8c836930ed130e020e77d9b2e65dd0fbab1b67c790b0f5d80b11a575" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/packages/system-query/Cargo.toml b/packages/system-query/Cargo.toml new file mode 100644 index 000000000..eb5f616dc --- /dev/null +++ b/packages/system-query/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "system-query" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +itertools = "0.11.0" +serde = { version = "1.0.188", features = ["derive"] } +serde-wasm-bindgen = "0.5.0" +wasm-bindgen = "0.2.87" + +[dev-dependencies] +rand = "0.8.5" +wasm-bindgen-test = "0.3.37" +serde_json = "1.0.105" +criterion = { version = "0.5" } + +[[bench]] +name = "basic" +harness = false \ No newline at end of file diff --git a/packages/system-query/README.md b/packages/system-query/README.md new file mode 100644 index 000000000..f38914633 --- /dev/null +++ b/packages/system-query/README.md @@ -0,0 +1 @@ +# system-query diff --git a/packages/system-query/benches/basic.rs b/packages/system-query/benches/basic.rs new file mode 100644 index 000000000..1e48e6ab9 --- /dev/null +++ b/packages/system-query/benches/basic.rs @@ -0,0 +1,65 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use rand::prelude::*; +use std::collections::HashSet; +use system_query::diff::diff_filter; +use system_query::filter::{FlatReqFilter, ReqFilter}; + +fn random_pubkey(rng: &mut ThreadRng) -> String { + let mut bytes = [0u8; 32]; + rng.fill_bytes(&mut bytes); + bytes.iter().map(|byte| format!("{:02x}", byte)).collect() +} + +fn criterion_benchmark(c: &mut Criterion) { + let mut rng = thread_rng(); + let mut random_pubkeys = HashSet::new(); + for _ in 0..50 { + random_pubkeys.insert(random_pubkey(&mut rng)); + } + let input_authors = ReqFilter { + authors: Some(random_pubkeys.clone()), + kinds: Some(HashSet::from([1, 2, 3])), + ids: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + e_tag: None, + }; + + let input_authors_diff = ReqFilter { + authors: Some(random_pubkeys.clone()), + kinds: Some(HashSet::from([1, 2, 3, 4, 5])), + ids: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + e_tag: None, + }; + + + c.bench_function("expand", |b| { + b.iter(|| { + let _: Vec = (&input_authors).into(); + }) + }); + c.bench_function("diff", |b| { + b.iter(|| { + let prev: Vec = (&input_authors).into(); + let next: Vec = (&input_authors_diff).into(); + let _ = diff_filter(&prev, &next); + }) + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/packages/system-query/package.json b/packages/system-query/package.json new file mode 100644 index 000000000..f3ce5ed2f --- /dev/null +++ b/packages/system-query/package.json @@ -0,0 +1,15 @@ +{ + "name": "@snort/system-query", + "version": "0.1.0", + "packageManager": "yarn@3.6.3", + "scripts": { + "build": "wasm-pack build -t web -s snort" + }, + "files": [ + "pkg/system_query_bg.wasm", + "pkg/system_query.js", + "pkg/system_query.d.ts" + ], + "module": "pkg/system_query.js", + "types": "pkg/system_query.d.ts" +} diff --git a/packages/system-query/pkg/README.md b/packages/system-query/pkg/README.md new file mode 100644 index 000000000..f38914633 --- /dev/null +++ b/packages/system-query/pkg/README.md @@ -0,0 +1 @@ +# system-query diff --git a/packages/system-query/pkg/package.json b/packages/system-query/pkg/package.json new file mode 100644 index 000000000..74743550f --- /dev/null +++ b/packages/system-query/pkg/package.json @@ -0,0 +1,14 @@ +{ + "name": "@snort/system-query", + "version": "0.1.0", + "files": [ + "system_query_bg.wasm", + "system_query.js", + "system_query.d.ts" + ], + "module": "system_query.js", + "types": "system_query.d.ts", + "sideEffects": [ + "./snippets/*" + ] +} diff --git a/packages/system-query/pkg/system_query.d.ts b/packages/system-query/pkg/system_query.d.ts new file mode 100644 index 000000000..b1781e0da --- /dev/null +++ b/packages/system-query/pkg/system_query.d.ts @@ -0,0 +1,65 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * @param {any} prev + * @param {any} next + * @returns {any} + */ +export function diff_filters(prev: any, next: any): any; +/** + * @param {any} val + * @returns {any} + */ +export function expand_filter(val: any): any; +/** + * @param {any} prev + * @param {any} next + * @returns {any} + */ +export function get_diff(prev: any, next: any): any; +/** + * @param {any} val + * @returns {any} + */ +export function flat_merge(val: any): any; +/** + * @param {any} val + * @returns {any} + */ +export function compress(val: any): any; + +export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; + +export interface InitOutput { + readonly memory: WebAssembly.Memory; + readonly diff_filters: (a: number, b: number, c: number) => void; + readonly expand_filter: (a: number, b: number) => void; + readonly get_diff: (a: number, b: number, c: number) => void; + readonly flat_merge: (a: number, b: number) => void; + readonly compress: (a: number, b: number) => void; + readonly __wbindgen_malloc: (a: number, b: number) => number; + readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; + readonly __wbindgen_add_to_stack_pointer: (a: number) => number; + readonly __wbindgen_exn_store: (a: number) => void; +} + +export type SyncInitInput = BufferSource | WebAssembly.Module; +/** + * Instantiates the given `module`, which can either be bytes or + * a precompiled `WebAssembly.Module`. + * + * @param {SyncInitInput} module + * + * @returns {InitOutput} + */ +export function initSync(module: SyncInitInput): InitOutput; + +/** + * If `module_or_path` is {RequestInfo} or {URL}, makes a request and + * for everything else, calls `WebAssembly.instantiate` directly. + * + * @param {InitInput | Promise} module_or_path + * + * @returns {Promise} + */ +export default function __wbg_init(module_or_path?: InitInput | Promise): Promise; diff --git a/packages/system-query/pkg/system_query.js b/packages/system-query/pkg/system_query.js new file mode 100644 index 000000000..67836cbdf --- /dev/null +++ b/packages/system-query/pkg/system_query.js @@ -0,0 +1,586 @@ +let wasm; + +const heap = new Array(128).fill(undefined); + +heap.push(undefined, null, true, false); + +function getObject(idx) { + return heap[idx]; +} + +let heap_next = heap.length; + +function dropObject(idx) { + if (idx < 132) return; + heap[idx] = heap_next; + heap_next = idx; +} + +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} + +let WASM_VECTOR_LEN = 0; + +let cachedUint8Memory0 = null; + +function getUint8Memory0() { + if (cachedUint8Memory0 === null || cachedUint8Memory0.byteLength === 0) { + cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8Memory0; +} + +const cachedTextEncoder = + typeof TextEncoder !== "undefined" + ? new TextEncoder("utf-8") + : { + encode: () => { + throw Error("TextEncoder not available"); + }, + }; + +const encodeString = + typeof cachedTextEncoder.encodeInto === "function" + ? function (arg, view) { + return cachedTextEncoder.encodeInto(arg, view); + } + : function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length, + }; + }; + +function passStringToWasm0(arg, malloc, realloc) { + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8Memory0() + .subarray(ptr, ptr + buf.length) + .set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8Memory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7f) break; + mem[ptr + offset] = code; + } + + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, (len = offset + arg.length * 3), 1) >>> 0; + const view = getUint8Memory0().subarray(ptr + offset, ptr + len); + const ret = encodeString(arg, view); + + offset += ret.written; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +let cachedInt32Memory0 = null; + +function getInt32Memory0() { + if (cachedInt32Memory0 === null || cachedInt32Memory0.byteLength === 0) { + cachedInt32Memory0 = new Int32Array(wasm.memory.buffer); + } + return cachedInt32Memory0; +} + +const cachedTextDecoder = + typeof TextDecoder !== "undefined" + ? new TextDecoder("utf-8", { ignoreBOM: true, fatal: true }) + : { + decode: () => { + throw Error("TextDecoder not available"); + }, + }; + +if (typeof TextDecoder !== "undefined") { + cachedTextDecoder.decode(); +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); +} + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} + +let cachedFloat64Memory0 = null; + +function getFloat64Memory0() { + if (cachedFloat64Memory0 === null || cachedFloat64Memory0.byteLength === 0) { + cachedFloat64Memory0 = new Float64Array(wasm.memory.buffer); + } + return cachedFloat64Memory0; +} + +function debugString(val) { + // primitive types + const type = typeof val; + if (type == "number" || type == "boolean" || val == null) { + return `${val}`; + } + if (type == "string") { + return `"${val}"`; + } + if (type == "symbol") { + const description = val.description; + if (description == null) { + return "Symbol"; + } else { + return `Symbol(${description})`; + } + } + if (type == "function") { + const name = val.name; + if (typeof name == "string" && name.length > 0) { + return `Function(${name})`; + } else { + return "Function"; + } + } + // objects + if (Array.isArray(val)) { + const length = val.length; + let debug = "["; + if (length > 0) { + debug += debugString(val[0]); + } + for (let i = 1; i < length; i++) { + debug += ", " + debugString(val[i]); + } + debug += "]"; + return debug; + } + // Test for built-in + const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); + let className; + if (builtInMatches.length > 1) { + className = builtInMatches[1]; + } else { + // Failed to match the standard '[object ClassName]' + return toString.call(val); + } + if (className == "Object") { + // we're a user defined class or Object + // JSON.stringify avoids problems with cycles, and is generally much + // easier than looping through ownProperties of `val`. + try { + return "Object(" + JSON.stringify(val) + ")"; + } catch (_) { + return "Object"; + } + } + // errors + if (val instanceof Error) { + return `${val.name}: ${val.message}\n${val.stack}`; + } + // TODO we could test for more things here, like `Set`s and `Map`s. + return className; +} +/** + * @param {any} prev + * @param {any} next + * @returns {any} + */ +export function diff_filters(prev, next) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.diff_filters(retptr, addHeapObject(prev), addHeapObject(next)); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var r2 = getInt32Memory0()[retptr / 4 + 2]; + if (r2) { + throw takeObject(r1); + } + return takeObject(r0); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } +} + +/** + * @param {any} val + * @returns {any} + */ +export function expand_filter(val) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.expand_filter(retptr, addHeapObject(val)); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var r2 = getInt32Memory0()[retptr / 4 + 2]; + if (r2) { + throw takeObject(r1); + } + return takeObject(r0); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } +} + +/** + * @param {any} prev + * @param {any} next + * @returns {any} + */ +export function get_diff(prev, next) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.get_diff(retptr, addHeapObject(prev), addHeapObject(next)); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var r2 = getInt32Memory0()[retptr / 4 + 2]; + if (r2) { + throw takeObject(r1); + } + return takeObject(r0); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } +} + +/** + * @param {any} val + * @returns {any} + */ +export function flat_merge(val) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.flat_merge(retptr, addHeapObject(val)); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var r2 = getInt32Memory0()[retptr / 4 + 2]; + if (r2) { + throw takeObject(r1); + } + return takeObject(r0); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } +} + +/** + * @param {any} val + * @returns {any} + */ +export function compress(val) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.compress(retptr, addHeapObject(val)); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var r2 = getInt32Memory0()[retptr / 4 + 2]; + if (r2) { + throw takeObject(r1); + } + return takeObject(r0); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } +} + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + wasm.__wbindgen_exn_store(addHeapObject(e)); + } +} + +async function __wbg_load(module, imports) { + if (typeof Response === "function" && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === "function") { + try { + return await WebAssembly.instantiateStreaming(module, imports); + } catch (e) { + if (module.headers.get("Content-Type") != "application/wasm") { + console.warn( + "`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", + e, + ); + } else { + throw e; + } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + } else { + return instance; + } + } +} + +function __wbg_get_imports() { + const imports = {}; + imports.wbg = {}; + imports.wbg.__wbindgen_object_drop_ref = function (arg0) { + takeObject(arg0); + }; + imports.wbg.__wbindgen_string_get = function (arg0, arg1) { + const obj = getObject(arg1); + const ret = typeof obj === "string" ? obj : undefined; + var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len1 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len1; + getInt32Memory0()[arg0 / 4 + 0] = ptr1; + }; + imports.wbg.__wbindgen_is_object = function (arg0) { + const val = getObject(arg0); + const ret = typeof val === "object" && val !== null; + return ret; + }; + imports.wbg.__wbindgen_is_undefined = function (arg0) { + const ret = getObject(arg0) === undefined; + return ret; + }; + imports.wbg.__wbindgen_in = function (arg0, arg1) { + const ret = getObject(arg0) in getObject(arg1); + return ret; + }; + imports.wbg.__wbindgen_error_new = function (arg0, arg1) { + const ret = new Error(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_object_clone_ref = function (arg0) { + const ret = getObject(arg0); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_jsval_loose_eq = function (arg0, arg1) { + const ret = getObject(arg0) == getObject(arg1); + return ret; + }; + imports.wbg.__wbindgen_boolean_get = function (arg0) { + const v = getObject(arg0); + const ret = typeof v === "boolean" ? (v ? 1 : 0) : 2; + return ret; + }; + imports.wbg.__wbindgen_number_get = function (arg0, arg1) { + const obj = getObject(arg1); + const ret = typeof obj === "number" ? obj : undefined; + getFloat64Memory0()[arg0 / 8 + 1] = isLikeNone(ret) ? 0 : ret; + getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret); + }; + imports.wbg.__wbindgen_number_new = function (arg0) { + const ret = arg0; + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_string_new = function (arg0, arg1) { + const ret = getStringFromWasm0(arg0, arg1); + return addHeapObject(ret); + }; + imports.wbg.__wbg_getwithrefkey_5e6d9547403deab8 = function (arg0, arg1) { + const ret = getObject(arg0)[getObject(arg1)]; + return addHeapObject(ret); + }; + imports.wbg.__wbg_set_841ac57cff3d672b = function (arg0, arg1, arg2) { + getObject(arg0)[takeObject(arg1)] = takeObject(arg2); + }; + imports.wbg.__wbg_get_44be0491f933a435 = function (arg0, arg1) { + const ret = getObject(arg0)[arg1 >>> 0]; + return addHeapObject(ret); + }; + imports.wbg.__wbg_length_fff51ee6522a1a18 = function (arg0) { + const ret = getObject(arg0).length; + return ret; + }; + imports.wbg.__wbg_new_898a68150f225f2e = function () { + const ret = new Array(); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_is_function = function (arg0) { + const ret = typeof getObject(arg0) === "function"; + return ret; + }; + imports.wbg.__wbg_next_526fc47e980da008 = function (arg0) { + const ret = getObject(arg0).next; + return addHeapObject(ret); + }; + imports.wbg.__wbg_next_ddb3312ca1c4e32a = function () { + return handleError(function (arg0) { + const ret = getObject(arg0).next(); + return addHeapObject(ret); + }, arguments); + }; + imports.wbg.__wbg_done_5c1f01fb660d73b5 = function (arg0) { + const ret = getObject(arg0).done; + return ret; + }; + imports.wbg.__wbg_value_1695675138684bd5 = function (arg0) { + const ret = getObject(arg0).value; + return addHeapObject(ret); + }; + imports.wbg.__wbg_iterator_97f0c81209c6c35a = function () { + const ret = Symbol.iterator; + return addHeapObject(ret); + }; + imports.wbg.__wbg_get_97b561fb56f034b5 = function () { + return handleError(function (arg0, arg1) { + const ret = Reflect.get(getObject(arg0), getObject(arg1)); + return addHeapObject(ret); + }, arguments); + }; + imports.wbg.__wbg_call_cb65541d95d71282 = function () { + return handleError(function (arg0, arg1) { + const ret = getObject(arg0).call(getObject(arg1)); + return addHeapObject(ret); + }, arguments); + }; + imports.wbg.__wbg_new_b51585de1b234aff = function () { + const ret = new Object(); + return addHeapObject(ret); + }; + imports.wbg.__wbg_set_502d29070ea18557 = function (arg0, arg1, arg2) { + getObject(arg0)[arg1 >>> 0] = takeObject(arg2); + }; + imports.wbg.__wbg_isArray_4c24b343cb13cfb1 = function (arg0) { + const ret = Array.isArray(getObject(arg0)); + return ret; + }; + imports.wbg.__wbg_instanceof_ArrayBuffer_39ac22089b74fddb = function (arg0) { + let result; + try { + result = getObject(arg0) instanceof ArrayBuffer; + } catch { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_isSafeInteger_bb8e18dd21c97288 = function (arg0) { + const ret = Number.isSafeInteger(getObject(arg0)); + return ret; + }; + imports.wbg.__wbg_buffer_085ec1f694018c4f = function (arg0) { + const ret = getObject(arg0).buffer; + return addHeapObject(ret); + }; + imports.wbg.__wbg_new_8125e318e6245eed = function (arg0) { + const ret = new Uint8Array(getObject(arg0)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_set_5cf90238115182c3 = function (arg0, arg1, arg2) { + getObject(arg0).set(getObject(arg1), arg2 >>> 0); + }; + imports.wbg.__wbg_length_72e2208bbc0efc61 = function (arg0) { + const ret = getObject(arg0).length; + return ret; + }; + imports.wbg.__wbg_instanceof_Uint8Array_d8d9cb2b8e8ac1d4 = function (arg0) { + let result; + try { + result = getObject(arg0) instanceof Uint8Array; + } catch { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbindgen_debug_string = function (arg0, arg1) { + const ret = debugString(getObject(arg1)); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len1; + getInt32Memory0()[arg0 / 4 + 0] = ptr1; + }; + imports.wbg.__wbindgen_throw = function (arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }; + imports.wbg.__wbindgen_memory = function () { + const ret = wasm.memory; + return addHeapObject(ret); + }; + + return imports; +} + +function __wbg_init_memory(imports, maybe_memory) {} + +function __wbg_finalize_init(instance, module) { + wasm = instance.exports; + __wbg_init.__wbindgen_wasm_module = module; + cachedFloat64Memory0 = null; + cachedInt32Memory0 = null; + cachedUint8Memory0 = null; + + return wasm; +} + +function initSync(module) { + if (wasm !== undefined) return wasm; + + const imports = __wbg_get_imports(); + + __wbg_init_memory(imports); + + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + + const instance = new WebAssembly.Instance(module, imports); + + return __wbg_finalize_init(instance, module); +} + +async function __wbg_init(input) { + if (wasm !== undefined) return wasm; + + if (typeof input === "undefined") { + input = new URL("system_query_bg.wasm", import.meta.url); + } + const imports = __wbg_get_imports(); + + if ( + typeof input === "string" || + (typeof Request === "function" && input instanceof Request) || + (typeof URL === "function" && input instanceof URL) + ) { + input = fetch(input); + } + + __wbg_init_memory(imports); + + const { instance, module } = await __wbg_load(await input, imports); + + return __wbg_finalize_init(instance, module); +} + +export { initSync }; +export default __wbg_init; diff --git a/packages/system-query/pkg/system_query_bg.wasm b/packages/system-query/pkg/system_query_bg.wasm new file mode 100644 index 000000000..9ab1834bc Binary files /dev/null and b/packages/system-query/pkg/system_query_bg.wasm differ diff --git a/packages/system-query/pkg/system_query_bg.wasm.d.ts b/packages/system-query/pkg/system_query_bg.wasm.d.ts new file mode 100644 index 000000000..3071e5fd3 --- /dev/null +++ b/packages/system-query/pkg/system_query_bg.wasm.d.ts @@ -0,0 +1,12 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export function diff_filters(a: number, b: number, c: number): void; +export function expand_filter(a: number, b: number): void; +export function get_diff(a: number, b: number, c: number): void; +export function flat_merge(a: number, b: number): void; +export function compress(a: number, b: number): void; +export function __wbindgen_malloc(a: number, b: number): number; +export function __wbindgen_realloc(a: number, b: number, c: number, d: number): number; +export function __wbindgen_add_to_stack_pointer(a: number): number; +export function __wbindgen_exn_store(a: number): void; diff --git a/packages/system-query/src/diff.rs b/packages/system-query/src/diff.rs new file mode 100644 index 000000000..0e1a35d50 --- /dev/null +++ b/packages/system-query/src/diff.rs @@ -0,0 +1,172 @@ +use crate::FlatReqFilter; +use itertools::Itertools; + +pub fn diff_filter(prev: &Vec, next: &Vec) -> Vec { + let mut added: Vec = vec![]; + + for n in next.iter() { + if !prev.iter().contains(&n) { + added.push(n.clone()) + } + } + + added +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn simple_diff_same() { + let prev = vec![FlatReqFilter { + id: Some("a".to_owned()), + author: None, + kind: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }]; + let next = vec![FlatReqFilter { + id: Some("a".to_owned()), + author: None, + kind: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }]; + + let result = diff_filter(&prev, &next); + assert_eq!(result, vec![]) + } + + #[test] + fn simple_diff_add() { + let prev = vec![FlatReqFilter { + id: Some("a".to_owned()), + author: None, + kind: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }]; + let next = vec![ + FlatReqFilter { + id: Some("a".to_owned()), + author: None, + kind: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }, + FlatReqFilter { + id: Some("b".to_owned()), + author: None, + kind: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }, + ]; + + let result = diff_filter(&prev, &next); + assert_eq!( + result, + vec![FlatReqFilter { + id: Some("b".to_owned()), + author: None, + kind: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }] + ) + } + + #[test] + fn simple_diff_replace() { + let prev = vec![FlatReqFilter { + id: Some("a".to_owned()), + author: None, + kind: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }]; + let next = vec![FlatReqFilter { + id: Some("b".to_owned()), + author: None, + kind: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }]; + + let result = diff_filter(&prev, &next); + assert_eq!( + result, + vec![FlatReqFilter { + id: Some("b".to_owned()), + author: None, + kind: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }] + ) + } +} diff --git a/packages/system-query/src/filter.rs b/packages/system-query/src/filter.rs new file mode 100644 index 000000000..333b2ee86 --- /dev/null +++ b/packages/system-query/src/filter.rs @@ -0,0 +1,732 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +#[cfg(test)] +use std::fmt::Debug; +use std::hash::Hash; +use itertools::Itertools; + +#[derive(Clone)] +enum StringOrNumberEntry<'a> { + String((&'static str, &'a String)), + Number((&'static str, &'a i32)), +} + +#[derive(PartialEq, Clone, Serialize, Deserialize)] +pub struct ReqFilter { + #[serde(rename = "ids", skip_serializing_if = "Option::is_none")] + pub ids: Option>, + #[serde(rename = "authors", skip_serializing_if = "Option::is_none")] + pub authors: Option>, + #[serde(rename = "kinds", skip_serializing_if = "Option::is_none")] + pub kinds: Option>, + #[serde(rename = "#e", skip_serializing_if = "Option::is_none")] + pub e_tag: Option>, + #[serde(rename = "#p", skip_serializing_if = "Option::is_none")] + pub p_tag: Option>, + #[serde(rename = "#t", skip_serializing_if = "Option::is_none")] + pub t_tag: Option>, + #[serde(rename = "#d", skip_serializing_if = "Option::is_none")] + pub d_tag: Option>, + #[serde(rename = "#r", skip_serializing_if = "Option::is_none")] + pub r_tag: Option>, + #[serde(rename = "search", skip_serializing_if = "Option::is_none")] + pub search: Option>, + #[serde(rename = "since", skip_serializing_if = "Option::is_none")] + pub since: Option, + #[serde(rename = "until", skip_serializing_if = "Option::is_none")] + pub until: Option, + #[serde(rename = "limit", skip_serializing_if = "Option::is_none")] + pub limit: Option, +} + +#[cfg(test)] +impl Debug for ReqFilter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&serde_json::to_string(self).unwrap().to_owned()) + } +} + +#[derive(PartialEq, PartialOrd, Clone, Serialize, Deserialize)] +pub struct FlatReqFilter { + #[serde(rename = "ids", skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(rename = "authors", skip_serializing_if = "Option::is_none")] + pub author: Option, + #[serde(rename = "kinds", skip_serializing_if = "Option::is_none")] + pub kind: Option, + #[serde(rename = "#e", skip_serializing_if = "Option::is_none")] + pub e_tag: Option, + #[serde(rename = "#p", skip_serializing_if = "Option::is_none")] + pub p_tag: Option, + #[serde(rename = "#t", skip_serializing_if = "Option::is_none")] + pub t_tag: Option, + #[serde(rename = "#d", skip_serializing_if = "Option::is_none")] + pub d_tag: Option, + #[serde(rename = "#r", skip_serializing_if = "Option::is_none")] + pub r_tag: Option, + #[serde(rename = "search", skip_serializing_if = "Option::is_none")] + pub search: Option, + #[serde(rename = "since", skip_serializing_if = "Option::is_none")] + pub since: Option, + #[serde(rename = "until", skip_serializing_if = "Option::is_none")] + pub until: Option, + #[serde(rename = "limit", skip_serializing_if = "Option::is_none")] + pub limit: Option, +} + +#[cfg(test)] +impl Debug for FlatReqFilter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&serde_json::to_string(self).unwrap().to_owned()) + } +} + +pub trait Distance { + /// Calculate the distance in terms of similarity for merging + /// + /// The goal of this function is to find 2 filters which are very similar where + /// one filter may have a single property change like so: + /// + /// ```javascript + /// const a = { "kinds": 1, "authors": "a", "since": 99 }; + /// const b = { "kinds": 1, "authors": "b", "since": 99 }; + /// ``` + /// In this case these 2 filters could be merged because their distance is `1` + /// ```javascript + /// const result = { "kinds": [1], "authors": ["a", "b"], "since": 99 }; + /// ``` + fn distance(&self, other: &Self) -> u32; +} + +pub trait CanMerge { + fn can_merge(&self, other: &Self) -> bool; +} + +impl Distance for FlatReqFilter { + fn distance(&self, b: &Self) -> u32 { + let mut ret = 0u32; + + ret += prop_dist(&self.id, &b.id); + ret += prop_dist(&self.kind, &b.kind); + ret += prop_dist(&self.author, &b.author); + ret += prop_dist(&self.e_tag, &b.e_tag); + ret += prop_dist(&self.p_tag, &b.p_tag); + ret += prop_dist(&self.d_tag, &b.d_tag); + ret += prop_dist(&self.r_tag, &b.r_tag); + ret += prop_dist(&self.t_tag, &b.t_tag); + ret += prop_dist(&self.search, &b.search); + + ret + } +} + +impl CanMerge for FlatReqFilter { + fn can_merge(&self, other: &Self) -> bool { + if self.since != other.since + || self.until != other.until + || self.limit != other.limit + || self.search != other.search + { + return false; + } + + self.distance(other) <= 1 + } +} + +impl From> for ReqFilter { + fn from(value: Vec<&FlatReqFilter>) -> Self { + let ret = ReqFilter { + ids: None, + authors: None, + kinds: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }; + value.iter().fold(ret, |mut acc, x| { + array_prop_append(&x.id, &mut acc.ids); + array_prop_append(&x.author, &mut acc.authors); + array_prop_append(&x.kind, &mut acc.kinds); + array_prop_append(&x.e_tag, &mut acc.e_tag); + array_prop_append(&x.p_tag, &mut acc.p_tag); + array_prop_append(&x.t_tag, &mut acc.t_tag); + array_prop_append(&x.d_tag, &mut acc.d_tag); + array_prop_append(&x.r_tag, &mut acc.r_tag); + array_prop_append(&x.search, &mut acc.search); + acc.since = x.since; + acc.until = x.until; + acc.limit = x.limit; + + acc + }) + } +} + +impl From> for ReqFilter { + fn from(value: Vec<&ReqFilter>) -> Self { + let ret = ReqFilter { + ids: None, + authors: None, + kinds: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }; + value.iter().fold(ret, |mut acc, x| { + array_prop_append_vec(&x.ids, &mut acc.ids); + array_prop_append_vec(&x.authors, &mut acc.authors); + array_prop_append_vec(&x.kinds, &mut acc.kinds); + array_prop_append_vec(&x.e_tag, &mut acc.e_tag); + array_prop_append_vec(&x.p_tag, &mut acc.p_tag); + array_prop_append_vec(&x.t_tag, &mut acc.t_tag); + array_prop_append_vec(&x.d_tag, &mut acc.d_tag); + array_prop_append_vec(&x.r_tag, &mut acc.r_tag); + array_prop_append_vec(&x.search, &mut acc.search); + acc.since = x.since; + acc.until = x.until; + acc.limit = x.limit; + + acc + }) + } +} + +impl Into> for &ReqFilter { + fn into(self) -> Vec { + let mut ret: Vec = Vec::new(); + + let mut inputs: Vec> = vec![]; + if let Some(ids) = &self.ids { + let t_ids = ids + .iter() + .map(|z| StringOrNumberEntry::String(("id", z))) + .collect(); + inputs.push(t_ids); + } + if let Some(authors) = &self.authors { + let t_ids = authors + .iter() + .map(|z| StringOrNumberEntry::String(("author", z))) + .collect(); + inputs.push(t_ids); + } + if let Some(kinds) = &self.kinds { + let t_ids = kinds + .iter() + .map(|z| StringOrNumberEntry::Number(("kind", z))) + .collect(); + inputs.push(t_ids); + } + if let Some(e_tags) = &self.e_tag { + let t_ids = e_tags + .iter() + .map(|z| StringOrNumberEntry::String(("e_tag", z))) + .collect(); + inputs.push(t_ids); + } + if let Some(p_tags) = &self.p_tag { + let t_ids = p_tags + .iter() + .map(|z| StringOrNumberEntry::String(("p_tag", z))) + .collect(); + inputs.push(t_ids); + } + if let Some(d_tags) = &self.d_tag { + let t_ids = d_tags + .iter() + .map(|z| StringOrNumberEntry::String(("d_tag", z))) + .collect(); + inputs.push(t_ids); + } + if let Some(t_tags) = &self.t_tag { + let t_ids = t_tags + .iter() + .map(|z| StringOrNumberEntry::String(("t_tag", z))) + .collect(); + inputs.push(t_ids); + } + if let Some(r_tags) = &self.r_tag { + let t_ids = r_tags + .iter() + .map(|z| StringOrNumberEntry::String(("r_tag", z))) + .collect(); + inputs.push(t_ids); + } + if let Some(search) = &self.search { + let t_ids = search + .iter() + .map(|z| StringOrNumberEntry::String(("search", z))) + .collect(); + inputs.push(t_ids); + } + + for p in inputs.iter().multi_cartesian_product() { + ret.push(FlatReqFilter { + id: p.iter().find_map(|q| { + if let StringOrNumberEntry::String((k, v)) = q { + if (*k).eq("id") { + return Some((*v).to_string()); + } + } + None + }), + author: p.iter().find_map(|q| { + if let StringOrNumberEntry::String((k, v)) = q { + if (*k).eq("author") { + return Some((*v).to_string()); + } + } + None + }), + kind: p.iter().find_map(|q| { + if let StringOrNumberEntry::Number((k, v)) = q { + if (*k).eq("kind") { + return Some((*v).clone()); + } + } + None + }), + e_tag: p.iter().find_map(|q| { + if let StringOrNumberEntry::String((k, v)) = q { + if (*k).eq("e_tag") { + return Some((*v).to_string()); + } + } + None + }), + p_tag: p.iter().find_map(|q| { + if let StringOrNumberEntry::String((k, v)) = q { + if (*k).eq("p_tag") { + return Some((*v).to_string()); + } + } + None + }), + t_tag: p.iter().find_map(|q| { + if let StringOrNumberEntry::String((k, v)) = q { + if (*k).eq("t_tag") { + return Some((*v).to_string()); + } + } + None + }), + d_tag: p.iter().find_map(|q| { + if let StringOrNumberEntry::String((k, v)) = q { + if (*k).eq("d_tag") { + return Some((*v).to_string()); + } + } + None + }), + r_tag: p.iter().find_map(|q| { + if let StringOrNumberEntry::String((k, v)) = q { + if (*k).eq("r_tag") { + return Some((*v).to_string()); + } + } + None + }), + search: p.iter().find_map(|q| { + if let StringOrNumberEntry::String((k, v)) = q { + if (*k).eq("search") { + return Some((*v).to_string()); + } + } + None + }), + since: self.since, + until: self.until, + limit: self.limit, + }) + } + ret + } +} +impl Distance for ReqFilter { + fn distance(&self, b: &Self) -> u32 { + let mut ret = 0u32; + + ret += prop_dist_vec(&self.ids, &b.ids); + ret += prop_dist_vec(&self.kinds, &b.kinds); + ret += prop_dist_vec(&self.authors, &b.authors); + ret += prop_dist_vec(&self.e_tag, &b.e_tag); + ret += prop_dist_vec(&self.p_tag, &b.p_tag); + ret += prop_dist_vec(&self.d_tag, &b.d_tag); + ret += prop_dist_vec(&self.r_tag, &b.r_tag); + ret += prop_dist_vec(&self.t_tag, &b.t_tag); + ret += prop_dist_vec(&self.search, &b.search); + + ret + } +} + +impl CanMerge for ReqFilter { + fn can_merge(&self, other: &Self) -> bool { + if self.since != other.since + || self.until != other.until + || self.limit != other.limit + || self.search != other.search + { + return false; + } + + self.distance(other) <= 1 + } +} + +#[inline(always)] +fn prop_dist(a: &Option, b: &Option) -> u32 { + if (a.is_some() && b.is_none()) || (a.is_none() && b.is_some()) { + return 10; + } else if a.is_some() && a != b { + return 1; + } + 0 +} + +#[inline(always)] +fn prop_dist_vec(a: &Option>, b: &Option>) -> u32 { + if (a.is_some() && b.is_none()) || (a.is_none() && b.is_some()) { + return 10; + } + match (a, b) { + (Some(aa), Some(bb)) => { + if aa.len() != bb.len() { + 1 + } else if aa == bb { + 0 + } else { + 1 + } + } + (None, None) => 0, + _ => panic!("Should not reach here!"), + } +} + +#[inline(always)] +fn array_prop_append(val: &Option, arr: &mut Option>) { + if let Some(ap) = val { + if arr.is_none() { + *arr = Some(HashSet::from([ap.clone()])) + } else { + arr.as_mut().unwrap().insert(ap.clone()); + } + } +} + +#[inline(always)] +fn array_prop_append_vec( + val: &Option>, + arr: &mut Option>, +) { + if let Some(ap) = val { + if arr.is_none() { + *arr = Some(ap.clone()) + } else { + ap.iter().for_each(|v| { + arr.as_mut().unwrap().insert((*v).clone()); + }); + } + } +} + +#[cfg(test)] +mod tests { + use crate::ReqFilter; + use std::collections::HashSet; + use crate::filter::FlatReqFilter; + + #[test] + fn test_expand_filter() { + let input = ReqFilter { + authors: Some(HashSet::from([ + "a".to_owned(), + "b".to_owned(), + "c".to_owned(), + ])), + kinds: Some(HashSet::from([1, 2, 3])), + ids: Some(HashSet::from(["x".to_owned(), "y".to_owned()])), + p_tag: Some(HashSet::from(["a".to_owned()])), + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: Some(99), + until: None, + limit: Some(10), + e_tag: None, + }; + + let output : Vec = (&input).into(); + let expected = vec![ + FlatReqFilter { + author: Some("a".to_owned()), + kind: Some(1), + id: Some("x".to_owned()), + p_tag: Some("a".to_owned()), + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: Some(99), + until: None, + limit: Some(10), + e_tag: None, + }, + FlatReqFilter { + author: Some("a".to_owned()), + kind: Some(1), + id: Some("y".to_owned()), + p_tag: Some("a".to_owned()), + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: Some(99), + until: None, + limit: Some(10), + e_tag: None, + }, + FlatReqFilter { + author: Some("a".to_owned()), + kind: Some(2), + id: Some("x".to_owned()), + p_tag: Some("a".to_owned()), + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: Some(99), + until: None, + limit: Some(10), + e_tag: None, + }, + FlatReqFilter { + author: Some("a".to_owned()), + kind: Some(2), + id: Some("y".to_owned()), + p_tag: Some("a".to_owned()), + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: Some(99), + until: None, + limit: Some(10), + e_tag: None, + }, + FlatReqFilter { + author: Some("a".to_owned()), + kind: Some(3), + id: Some("x".to_owned()), + p_tag: Some("a".to_owned()), + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: Some(99), + until: None, + limit: Some(10), + e_tag: None, + }, + FlatReqFilter { + author: Some("a".to_owned()), + kind: Some(3), + id: Some("y".to_owned()), + p_tag: Some("a".to_owned()), + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: Some(99), + until: None, + limit: Some(10), + e_tag: None, + }, + FlatReqFilter { + author: Some("b".to_owned()), + kind: Some(1), + id: Some("x".to_owned()), + p_tag: Some("a".to_owned()), + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: Some(99), + until: None, + limit: Some(10), + e_tag: None, + }, + FlatReqFilter { + author: Some("b".to_owned()), + kind: Some(1), + id: Some("y".to_owned()), + p_tag: Some("a".to_owned()), + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: Some(99), + until: None, + limit: Some(10), + e_tag: None, + }, + FlatReqFilter { + author: Some("b".to_owned()), + kind: Some(2), + id: Some("x".to_owned()), + p_tag: Some("a".to_owned()), + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: Some(99), + until: None, + limit: Some(10), + e_tag: None, + }, + FlatReqFilter { + author: Some("b".to_owned()), + kind: Some(2), + id: Some("y".to_owned()), + p_tag: Some("a".to_owned()), + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: Some(99), + until: None, + limit: Some(10), + e_tag: None, + }, + FlatReqFilter { + author: Some("b".to_owned()), + kind: Some(3), + id: Some("x".to_owned()), + p_tag: Some("a".to_owned()), + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: Some(99), + until: None, + limit: Some(10), + e_tag: None, + }, + FlatReqFilter { + author: Some("b".to_owned()), + kind: Some(3), + id: Some("y".to_owned()), + p_tag: Some("a".to_owned()), + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: Some(99), + until: None, + limit: Some(10), + e_tag: None, + }, + FlatReqFilter { + author: Some("c".to_owned()), + kind: Some(1), + id: Some("x".to_owned()), + p_tag: Some("a".to_owned()), + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: Some(99), + until: None, + limit: Some(10), + e_tag: None, + }, + FlatReqFilter { + author: Some("c".to_owned()), + kind: Some(1), + id: Some("y".to_owned()), + p_tag: Some("a".to_owned()), + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: Some(99), + until: None, + limit: Some(10), + e_tag: None, + }, + FlatReqFilter { + author: Some("c".to_owned()), + kind: Some(2), + id: Some("x".to_owned()), + p_tag: Some("a".to_owned()), + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: Some(99), + until: None, + limit: Some(10), + e_tag: None, + }, + FlatReqFilter { + author: Some("c".to_owned()), + kind: Some(2), + id: Some("y".to_owned()), + p_tag: Some("a".to_owned()), + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: Some(99), + until: None, + limit: Some(10), + e_tag: None, + }, + FlatReqFilter { + author: Some("c".to_owned()), + kind: Some(3), + id: Some("x".to_owned()), + p_tag: Some("a".to_owned()), + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: Some(99), + until: None, + limit: Some(10), + e_tag: None, + }, + FlatReqFilter { + author: Some("c".to_owned()), + kind: Some(3), + id: Some("y".to_owned()), + p_tag: Some("a".to_owned()), + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: Some(99), + until: None, + limit: Some(10), + e_tag: None, + }, + ]; + assert_eq!(output.len(), expected.len()); + output.iter().for_each(|a| assert!(expected.contains(a))); + } +} diff --git a/packages/system-query/src/lib.rs b/packages/system-query/src/lib.rs new file mode 100644 index 000000000..001f2ecc2 --- /dev/null +++ b/packages/system-query/src/lib.rs @@ -0,0 +1,151 @@ +use crate::filter::{FlatReqFilter, ReqFilter}; +use wasm_bindgen::prelude::*; + +pub mod diff; +pub mod filter; +pub mod merge; + +#[wasm_bindgen] +pub fn diff_filters(prev: JsValue, next: JsValue) -> Result { + let prev_parsed: Vec = serde_wasm_bindgen::from_value(prev)?; + let next_parsed: Vec = serde_wasm_bindgen::from_value(next)?; + let result = diff::diff_filter(&prev_parsed, &next_parsed); + Ok(serde_wasm_bindgen::to_value(&result)?) +} + +#[wasm_bindgen] +pub fn expand_filter(val: JsValue) -> Result { + let parsed: ReqFilter = serde_wasm_bindgen::from_value(val)?; + let result: Vec = (&parsed).into(); + Ok(serde_wasm_bindgen::to_value(&result)?) +} + +#[wasm_bindgen] +pub fn get_diff(prev: JsValue, next: JsValue) -> Result { + let prev_parsed: Vec = serde_wasm_bindgen::from_value(prev)?; + let next_parsed: Vec = serde_wasm_bindgen::from_value(next)?; + let expanded_prev: Vec = prev_parsed + .iter() + .flat_map(|v| { + let vec: Vec = v.into(); + vec + }) + .collect(); + let expanded_next: Vec = next_parsed + .iter() + .flat_map(|v| { + let vec: Vec = v.into(); + vec + }) + .collect(); + let result = diff::diff_filter(&expanded_prev, &expanded_next); + Ok(serde_wasm_bindgen::to_value(&result)?) +} + +#[wasm_bindgen] +pub fn flat_merge(val: JsValue) -> Result { + let val_parsed: Vec = serde_wasm_bindgen::from_value(val)?; + let result = merge::merge::(val_parsed.iter().collect()); + Ok(serde_wasm_bindgen::to_value(&result)?) +} + +#[wasm_bindgen] +pub fn compress(val: JsValue) -> Result { + let val_parsed: Vec = serde_wasm_bindgen::from_value(val)?; + let result = merge::merge::(val_parsed.iter().collect()); + Ok(serde_wasm_bindgen::to_value(&result)?) +} + +#[cfg(test)] +mod tests { + use super::*; + use itertools::Itertools; + use std::cmp::Ordering; + use std::collections::HashSet; + + #[test] + fn flat_merge_expanded() { + let input = vec![ + ReqFilter { + ids: None, + kinds: Some(HashSet::from([1, 6969, 6])), + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + authors: Some(HashSet::from([ + "kieran".to_string(), + "snort".to_string(), + "c".to_string(), + "d".to_string(), + "e".to_string(), + ])), + since: Some(1), + until: Some(100), + search: None, + limit: None, + }, + ReqFilter { + ids: None, + kinds: Some(HashSet::from([4])), + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + authors: Some(HashSet::from(["kieran".to_string()])), + limit: None, + }, + ReqFilter { + ids: None, + authors: None, + kinds: Some(HashSet::from([4])), + e_tag: None, + p_tag: Some(HashSet::from(["kieran".to_string()])), + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }, + ReqFilter { + ids: None, + kinds: Some(HashSet::from([1000])), + authors: Some(HashSet::from(["snort".to_string()])), + p_tag: Some(HashSet::from(["kieran".to_string()])), + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + e_tag: None, + limit: None, + }, + ]; + + let expanded: Vec = input + .iter() + .flat_map(|v| { + let r: Vec = v.into(); + r + }) + .sorted_by(|_, _| { + if rand::random() { + Ordering::Less + } else { + Ordering::Greater + } + }) + .collect(); + let merged_expanded: Vec = merge::merge(expanded.iter().collect()); + assert_eq!(merged_expanded.len(), input.len()); + assert!(merged_expanded.iter().all(|v| input.contains(v))); + } +} diff --git a/packages/system-query/src/merge.rs b/packages/system-query/src/merge.rs new file mode 100644 index 000000000..b8e797bef --- /dev/null +++ b/packages/system-query/src/merge.rs @@ -0,0 +1,444 @@ +use crate::filter::CanMerge; + +pub fn merge<'a, T, Z>(all: Vec<&'a T>) -> Vec +where + T: CanMerge, + for<'b> Z: CanMerge + From> + From>, +{ + let mut ret: Vec = merge_once(all); + loop { + let last_len = ret.len(); + ret = merge_once(ret.iter().collect()); + if last_len == ret.len() { + break; + } + } + ret +} + +fn merge_once<'a, T, Z>(all: Vec<&'a T>) -> Vec +where + T: CanMerge, + Z: From>, +{ + let mut ret: Vec = vec![]; + if all.is_empty() { + return ret; + } + + let merge_sets: Vec> = vec![vec![all.first().unwrap()]]; + let merge_sets = all.iter().skip(1).fold(merge_sets, |mut acc, x| { + let mut did_match = false; + for y in acc.iter_mut() { + if y.iter().all(|z| z.can_merge(x)) { + y.push(x); + did_match = true; + break; + } + } + if !did_match { + acc.push(vec![x]); + } + acc + }); + + for s in merge_sets { + ret.push(Z::from(s)); + } + + ret +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::filter::{Distance, FlatReqFilter, ReqFilter}; + use std::collections::HashSet; + + #[test] + fn distance() { + let a = FlatReqFilter { + id: Some("a".to_owned()), + author: None, + kind: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }; + let b = FlatReqFilter { + id: Some("a".to_owned()), + author: None, + kind: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }; + let c = FlatReqFilter { + id: Some("c".to_owned()), + author: None, + kind: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }; + let d = FlatReqFilter { + id: Some("a".to_owned()), + author: None, + kind: Some(1), + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }; + let e = FlatReqFilter { + id: Some("e".to_owned()), + author: None, + kind: Some(1), + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }; + assert_eq!(a.distance(&b), 0); + assert_eq!(a.distance(&c), 1); + assert_eq!(a.distance(&d), 10); + assert_eq!(a.distance(&e), 11); + } + + #[test] + fn merge_set() { + let a = FlatReqFilter { + id: Some("0".to_owned()), + author: Some("a".to_owned()), + kind: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: Some(10), + }; + let b = FlatReqFilter { + id: Some("0".to_owned()), + author: Some("b".to_owned()), + kind: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: Some(10), + }; + + let output = ReqFilter { + ids: Some(HashSet::from(["0".to_owned()])), + authors: Some(HashSet::from(["a".to_owned(), "b".to_owned()])), + kinds: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: Some(10), + }; + assert_eq!(ReqFilter::from(vec![&a, &b]), output); + } + + #[test] + fn can_merge_filters() { + let a = FlatReqFilter { + id: Some("0".to_owned()), + author: Some("a".to_owned()), + kind: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: Some(10), + }; + let b = FlatReqFilter { + id: Some("0".to_owned()), + author: Some("b".to_owned()), + kind: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: Some(10), + }; + let c = FlatReqFilter { + id: Some("0".to_owned()), + author: Some("b".to_owned()), + kind: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: Some(100), + }; + assert!(&a.can_merge(&b)); + assert!(!&b.can_merge(&c)); + } + + #[test] + fn flat_merge() { + let input = vec![ + FlatReqFilter { + id: Some("0".to_owned()), + author: Some("a".to_owned()), + kind: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }, + FlatReqFilter { + id: Some("0".to_owned()), + author: Some("b".to_owned()), + kind: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }, + FlatReqFilter { + id: None, + author: None, + kind: Some(1), + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }, + FlatReqFilter { + id: None, + author: None, + kind: Some(2), + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }, + FlatReqFilter { + id: None, + author: None, + kind: Some(2), + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }, + FlatReqFilter { + id: Some("0".to_owned()), + author: Some("c".to_owned()), + kind: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }, + FlatReqFilter { + id: None, + author: Some("c".to_owned()), + kind: Some(1), + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }, + FlatReqFilter { + id: None, + author: Some("c".to_owned()), + kind: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: Some(100), + }, + FlatReqFilter { + id: Some("1".to_owned()), + author: Some("c".to_owned()), + kind: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }, + ]; + let output = vec![ + ReqFilter { + ids: Some(HashSet::from(["0".to_owned()])), + authors: Some(HashSet::from([ + "a".to_owned(), + "b".to_owned(), + "c".to_owned(), + ])), + kinds: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }, + ReqFilter { + ids: None, + authors: None, + kinds: Some(HashSet::from([1, 2])), + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }, + ReqFilter { + ids: None, + authors: Some(HashSet::from(["c".to_owned()])), + kinds: Some(HashSet::from([1])), + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }, + ReqFilter { + ids: None, + authors: Some(HashSet::from(["c".to_owned()])), + kinds: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: Some(100), + }, + ReqFilter { + ids: Some(HashSet::from(["1".to_owned()])), + authors: Some(HashSet::from(["c".to_owned()])), + kinds: None, + e_tag: None, + p_tag: None, + t_tag: None, + d_tag: None, + r_tag: None, + search: None, + since: None, + until: None, + limit: None, + }, + ]; + + assert_eq!( + merge::(input.iter().collect()), + output + ) + } +} diff --git a/packages/system-query/src/utils.rs b/packages/system-query/src/utils.rs new file mode 100644 index 000000000..e69de29bb diff --git a/packages/system-query/system-query.iml b/packages/system-query/system-query.iml new file mode 100644 index 000000000..75504b265 --- /dev/null +++ b/packages/system-query/system-query.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/system/package.json b/packages/system/package.json index 434683afe..b10f9a483 100644 --- a/packages/system/package.json +++ b/packages/system/package.json @@ -34,6 +34,7 @@ "@noble/hashes": "^1.3.2", "@scure/base": "^1.1.2", "@snort/shared": "^1.0.5", + "@snort/system-query": "workspace:*", "@stablelib/xchacha20": "^1.0.1", "debug": "^4.3.4", "dexie": "^3.2.4", diff --git a/packages/system/src/gossip-model.ts b/packages/system/src/gossip-model.ts index d931d6b66..2f2d9be82 100644 --- a/packages/system/src/gossip-model.ts +++ b/packages/system/src/gossip-model.ts @@ -1,7 +1,7 @@ import { ReqFilter, UsersRelays } from "."; import { dedupe, unwrap } from "@snort/shared"; import debug from "debug"; -import { FlatReqFilter } from "request-expander"; +import { FlatReqFilter } from "./query-optimizer"; const PickNRelays = 2; diff --git a/packages/system/src/index.ts b/packages/system/src/index.ts index dd1f1f1dc..cf8024c5a 100644 --- a/packages/system/src/index.ts +++ b/packages/system/src/index.ts @@ -1,9 +1,11 @@ import { AuthHandler, RelaySettings, ConnectionStateSnapshot } from "./connection"; import { RequestBuilder } from "./request-builder"; -import { NoteStore, NoteStoreHook, NoteStoreSnapshotData } from "./note-collection"; +import { NoteStore, NoteStoreSnapshotData } from "./note-collection"; import { Query } from "./query"; import { NostrEvent, ReqFilter, TaggedNostrEvent } from "./nostr"; import { ProfileLoaderService } from "./profile-cache"; +import { RelayCache } from "./gossip-model"; +import { QueryOptimizer } from "./query-optimizer"; export * from "./nostr-system"; export { default as EventKind } from "./event-kind"; @@ -24,6 +26,7 @@ export * from "./signer"; export * from "./text"; export * from "./pow"; export * from "./pow-util"; +export * from "./query-optimizer"; export * from "./impl/nip4"; export * from "./impl/nip44"; @@ -96,6 +99,16 @@ export interface SystemInterface { * Profile cache/loader */ get ProfileLoader(): ProfileLoaderService; + + /** + * Relay cache for "Gossip" model + */ + get RelayCache(): RelayCache; + + /** + * Query optimizer + */ + get QueryOptimizer(): QueryOptimizer; } export interface SystemSnapshot { diff --git a/packages/system/src/nostr-system.ts b/packages/system/src/nostr-system.ts index b0786757c..4382471e9 100644 --- a/packages/system/src/nostr-system.ts +++ b/packages/system/src/nostr-system.ts @@ -4,7 +4,7 @@ import { unwrap, sanitizeRelayUrl, ExternalStore, FeedCache } from "@snort/share import { NostrEvent, TaggedNostrEvent } from "./nostr"; import { AuthHandler, Connection, RelaySettings, ConnectionStateSnapshot } from "./connection"; import { Query } from "./query"; -import { NoteCollection, NoteStore, NoteStoreHook, NoteStoreSnapshotData } from "./note-collection"; +import { NoteCollection, NoteStore, NoteStoreSnapshotData } from "./note-collection"; import { BuiltRawReqFilter, RequestBuilder } from "./request-builder"; import { RelayMetricHandler } from "./relay-metric-handler"; import { @@ -20,6 +20,8 @@ import { UsersRelays, } from "."; import { EventsCache } from "./cache/events"; +import { RelayCache } from "./gossip-model"; +import { QueryOptimizer, DefaultQueryOptimizer } from "./query-optimizer"; /** * Manages nostr content retrieval system @@ -72,12 +74,18 @@ export class NostrSystem extends ExternalStore implements System */ #eventsCache: FeedCache; + /** + * Query optimizer instance + */ + #queryOptimizer: QueryOptimizer; + constructor(props: { authHandler?: AuthHandler; relayCache?: FeedCache; profileCache?: FeedCache; relayMetrics?: FeedCache; eventsCache?: FeedCache; + queryOptimizer?: QueryOptimizer; }) { super(); this.#handleAuth = props.authHandler; @@ -85,6 +93,7 @@ export class NostrSystem extends ExternalStore implements System this.#profileCache = props.profileCache ?? new UserProfileCache(); this.#relayMetricsCache = props.relayMetrics ?? new RelayMetricCache(); this.#eventsCache = props.eventsCache ?? new EventsCache(); + this.#queryOptimizer = props.queryOptimizer ?? DefaultQueryOptimizer; this.#profileLoader = new ProfileLoaderService(this, this.#profileCache); this.#relayMetrics = new RelayMetricHandler(this.#relayMetricsCache); @@ -92,9 +101,6 @@ export class NostrSystem extends ExternalStore implements System } HandleAuth?: AuthHandler | undefined; - /** - * Profile loader service allows you to request profiles - */ get ProfileLoader() { return this.#profileLoader; } @@ -103,6 +109,14 @@ export class NostrSystem extends ExternalStore implements System return [...this.#sockets.values()].map(a => a.snapshot()); } + get RelayCache(): RelayCache { + return this.#relayCache; + } + + get QueryOptimizer(): QueryOptimizer { + return this.#queryOptimizer; + } + /** * Setup caches */ @@ -240,9 +254,7 @@ export class NostrSystem extends ExternalStore implements System if (existing.fromInstance === req.instance) { return existing; } - const filters = !req.options?.skipDiff - ? req.buildDiff(this.#relayCache, existing.flatFilters) - : req.build(this.#relayCache); + const filters = !req.options?.skipDiff ? req.buildDiff(this, existing.filters) : req.build(this); if (filters.length === 0 && !!req.options?.skipDiff) { return existing; } else { @@ -255,11 +267,15 @@ export class NostrSystem extends ExternalStore implements System } else { const store = new type(); - const filters = req.build(this.#relayCache); + const filters = req.build(this); const q = new Query(req.id, req.instance, store, req.options?.leaveOpen); if (filters.some(a => a.filters.some(b => b.ids))) { + const expectIds = new Set(filters.flatMap(a => a.filters).flatMap(a => a.ids ?? [])); q.feed.onEvent(async evs => { - await this.#eventsCache.bulkSet(evs); + const toSet = evs.filter(a => expectIds.has(a.id) && this.#eventsCache.getFromCache(a.id) === undefined); + if (toSet.length > 0) { + await this.#eventsCache.bulkSet(toSet); + } }); } this.Queries.set(req.id, q); diff --git a/packages/system/src/query-optimizer/index.ts b/packages/system/src/query-optimizer/index.ts new file mode 100644 index 000000000..21d8b44a8 --- /dev/null +++ b/packages/system/src/query-optimizer/index.ts @@ -0,0 +1,46 @@ +import { ReqFilter } from "../nostr"; +import { expandFilter } from "./request-expander"; +import { flatMerge, mergeSimilar } from "./request-merger"; +import { diffFilters } from "./request-splitter"; + +export interface FlatReqFilter { + keys: number; + ids?: string; + authors?: string; + kinds?: number; + "#e"?: string; + "#p"?: string; + "#t"?: string; + "#d"?: string; + "#r"?: string; + search?: string; + since?: number; + until?: number; + limit?: number; +} + +export interface QueryOptimizer { + expandFilter(f: ReqFilter): Array; + getDiff(prev: Array, next: Array): Array; + flatMerge(all: Array): Array; + compress(all: Array): Array; +} + +export const DefaultQueryOptimizer = { + expandFilter: (f: ReqFilter) => { + return expandFilter(f); + }, + getDiff: (prev: Array, next: Array) => { + const diff = diffFilters( + prev.flatMap(a => expandFilter(a)), + next.flatMap(a => expandFilter(a)), + ); + return diff.added; + }, + flatMerge: (all: Array) => { + return flatMerge(all); + }, + compress: (all: Array) => { + return mergeSimilar(all); + }, +} as QueryOptimizer; diff --git a/packages/system/src/request-expander.ts b/packages/system/src/query-optimizer/request-expander.ts similarity index 75% rename from packages/system/src/request-expander.ts rename to packages/system/src/query-optimizer/request-expander.ts index 669c55a34..3dfdb5ef7 100644 --- a/packages/system/src/request-expander.ts +++ b/packages/system/src/query-optimizer/request-expander.ts @@ -1,20 +1,5 @@ -import { ReqFilter } from "./nostr"; - -export interface FlatReqFilter { - keys: number; - ids?: string; - authors?: string; - kinds?: number; - "#e"?: string; - "#p"?: string; - "#t"?: string; - "#d"?: string; - "#r"?: string; - search?: string; - since?: number; - until?: number; - limit?: number; -} +import { FlatReqFilter } from "."; +import { ReqFilter } from "../nostr"; /** * Expand a filter into its most fine grained form diff --git a/packages/system/src/request-merger.ts b/packages/system/src/query-optimizer/request-merger.ts similarity index 96% rename from packages/system/src/request-merger.ts rename to packages/system/src/query-optimizer/request-merger.ts index a345ef034..5ed3900e1 100644 --- a/packages/system/src/request-merger.ts +++ b/packages/system/src/query-optimizer/request-merger.ts @@ -1,6 +1,6 @@ import { distance } from "@snort/shared"; -import { ReqFilter } from "."; -import { FlatReqFilter } from "./request-expander"; +import { ReqFilter } from ".."; +import { FlatReqFilter } from "."; /** * Keys which can change the entire meaning of the filter outside the array types @@ -105,7 +105,7 @@ export function flatMerge(all: Array): Array { function mergeFiltersInSet(filters: Array) { return filters.reduce((acc, a) => { Object.entries(a).forEach(([k, v]) => { - if (k === "keys") return; + if (k === "keys" || v === undefined) return; if (DiscriminatorKeys.includes(k)) { acc[k] = v; } else { diff --git a/packages/system/src/request-splitter.ts b/packages/system/src/query-optimizer/request-splitter.ts similarity index 88% rename from packages/system/src/request-splitter.ts rename to packages/system/src/query-optimizer/request-splitter.ts index 7f51295df..623a2798d 100644 --- a/packages/system/src/request-splitter.ts +++ b/packages/system/src/query-optimizer/request-splitter.ts @@ -1,5 +1,5 @@ -import { flatFilterEq } from "./utils"; -import { FlatReqFilter } from "./request-expander"; +import { flatFilterEq } from "../utils"; +import { FlatReqFilter } from "."; export function diffFilters(prev: Array, next: Array, calcRemoved?: boolean) { const added = []; diff --git a/packages/system/src/query.ts b/packages/system/src/query.ts index 0be31a462..065b0468b 100644 --- a/packages/system/src/query.ts +++ b/packages/system/src/query.ts @@ -4,9 +4,7 @@ import { unixNowMs, unwrap } from "@snort/shared"; import { Connection, ReqFilter, Nips, TaggedNostrEvent } from "."; import { NoteStore } from "./note-collection"; -import { flatMerge } from "./request-merger"; import { BuiltRawReqFilter } from "./request-builder"; -import { FlatReqFilter, expandFilter } from "./request-expander"; import { eventMatchesFilter } from "./request-matcher"; /** @@ -19,7 +17,6 @@ class QueryTrace { eose?: number; close?: number; #wasForceClosed = false; - readonly flatFilters: Array; readonly #fnClose: (id: string) => void; readonly #fnProgress: () => void; @@ -34,7 +31,6 @@ class QueryTrace { this.start = unixNowMs(); this.#fnClose = fnClose; this.#fnProgress = fnProgress; - this.flatFilters = filters.flatMap(expandFilter); } sentToRelay() { @@ -166,11 +162,7 @@ export class Query implements QueryBase { * Recompute the complete set of compressed filters from all query traces */ get filters() { - return flatMerge(this.flatFilters); - } - - get flatFilters() { - return this.#tracing.flatMap(a => a.flatFilters); + return this.#tracing.flatMap(a => a.filters); } get feed() { diff --git a/packages/system/src/request-builder.ts b/packages/system/src/request-builder.ts index 747a97a4a..dcd0ba668 100644 --- a/packages/system/src/request-builder.ts +++ b/packages/system/src/request-builder.ts @@ -2,11 +2,10 @@ import debug from "debug"; import { v4 as uuid } from "uuid"; import { appendDedupe, sanitizeRelayUrl, unixNowMs } from "@snort/shared"; -import { ReqFilter, u256, HexKey, EventKind } from "."; -import { diffFilters } from "./request-splitter"; +import EventKind from "./event-kind"; +import { SystemInterface } from "index"; +import { ReqFilter, u256, HexKey } from "./nostr"; import { RelayCache, splitByWriteRelays, splitFlatByWriteRelays } from "./gossip-model"; -import { flatMerge, mergeSimilar } from "./request-merger"; -import { FlatReqFilter, expandFilter } from "./request-expander"; /** * Which strategy is used when building REQ filters @@ -95,26 +94,25 @@ export class RequestBuilder { return this.#builders.map(f => f.filter); } - build(relays: RelayCache): Array { - const expanded = this.#builders.flatMap(a => a.build(relays, this.id)); - return this.#groupByRelay(expanded); + build(system: SystemInterface): Array { + const expanded = this.#builders.flatMap(a => a.build(system.RelayCache, this.id)); + return this.#groupByRelay(system, expanded); } /** * Detects a change in request from a previous set of filters */ - buildDiff(relays: RelayCache, prev: Array): Array { + buildDiff(system: SystemInterface, prev: Array): Array { const start = unixNowMs(); - const next = this.#builders.flatMap(f => expandFilter(f.filter)); - const diff = diffFilters(prev, next); + const diff = system.QueryOptimizer.getDiff(prev, this.buildRaw()); const ts = unixNowMs() - start; this.#log("buildDiff %s %d ms", this.id, ts); - if (diff.changed) { - return splitFlatByWriteRelays(relays, diff.added).map(a => { + if (diff.length > 0) { + return splitFlatByWriteRelays(system.RelayCache, diff).map(a => { return { strategy: RequestStrategy.AuthorsRelays, - filters: flatMerge(a.filters), + filters: system.QueryOptimizer.flatMerge(a.filters), relay: a.relay, }; }); @@ -129,7 +127,7 @@ export class RequestBuilder { * @param expanded * @returns */ - #groupByRelay(expanded: Array) { + #groupByRelay(system: SystemInterface, expanded: Array) { const relayMerged = expanded.reduce((acc, v) => { const existing = acc.get(v.relay); if (existing) { @@ -142,7 +140,7 @@ export class RequestBuilder { const filtersSquashed = [...relayMerged.values()].map(a => { return { - filters: mergeSimilar(a.flatMap(b => b.filters)), + filters: system.QueryOptimizer.compress(a.flatMap(b => b.filters)), relay: a[0].relay, strategy: a[0].strategy, } as BuiltRawReqFilter; diff --git a/packages/system/src/system-worker.ts b/packages/system/src/system-worker.ts index 622f03dd6..d23229310 100644 --- a/packages/system/src/system-worker.ts +++ b/packages/system/src/system-worker.ts @@ -6,6 +6,8 @@ import { NostrEvent, TaggedNostrEvent } from "./nostr"; import { NoteStore, NoteStoreSnapshotData } from "./note-collection"; import { Query } from "./query"; import { RequestBuilder } from "./request-builder"; +import { RelayCache } from "./gossip-model"; +import { QueryOptimizer } from "./query-optimizer"; export class SystemWorker extends ExternalStore implements SystemInterface { #port: MessagePort; @@ -29,6 +31,13 @@ export class SystemWorker extends ExternalStore implements Syste throw new Error("Method not implemented."); } + get RelayCache(): RelayCache { + throw new Error("Method not implemented."); + } + + get QueryOptimizer(): QueryOptimizer { + throw new Error("Method not implemented."); + } HandleAuth?: AuthHandler; get Sockets(): ConnectionStateSnapshot[] { diff --git a/packages/system/src/utils.ts b/packages/system/src/utils.ts index ec9a2b353..319e30c00 100644 --- a/packages/system/src/utils.ts +++ b/packages/system/src/utils.ts @@ -1,5 +1,5 @@ import { equalProp } from "@snort/shared"; -import { FlatReqFilter } from "./request-expander"; +import { FlatReqFilter } from "./query-optimizer"; import { NostrEvent, ReqFilter } from "./nostr"; export function findTag(e: NostrEvent, tag: string) { diff --git a/packages/system/src/zaps.ts b/packages/system/src/zaps.ts index 57c3b3551..bf9a65b5a 100644 --- a/packages/system/src/zaps.ts +++ b/packages/system/src/zaps.ts @@ -4,6 +4,8 @@ import { HexKey, NostrEvent } from "./nostr"; import { findTag } from "./utils"; import { MetadataCache } from "./cache"; +const ParsedZapCache = new Map(); + function getInvoice(zap: NostrEvent): InvoiceDetails | undefined { const bolt11 = findTag(zap, "bolt11"); if (!bolt11) { @@ -13,6 +15,11 @@ function getInvoice(zap: NostrEvent): InvoiceDetails | undefined { } export function parseZap(zapReceipt: NostrEvent, userCache: FeedCache, refNote?: NostrEvent): ParsedZap { + const existing = ParsedZapCache.get(zapReceipt.id); + if (existing) { + return existing; + } + let innerZapJson = findTag(zapReceipt, "description"); if (innerZapJson) { try { @@ -67,7 +74,7 @@ export function parseZap(zapReceipt: NostrEvent, userCache: FeedCache { - system.DisconnectRelay(Relay); - }, 1000); + await system.ConnectToRelay(Relay, { read: true, write: true }); + setTimeout(() => { + system.DisconnectRelay(Relay); + }, 1000); } -test().catch(console.error); \ No newline at end of file +test().catch(console.error); diff --git a/yarn.lock b/yarn.lock index ed4e9b980..9946756bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2711,6 +2711,7 @@ __metadata: "@scure/bip39": ^1.1.1 "@snort/shared": "workspace:*" "@snort/system": "workspace:*" + "@snort/system-query": "workspace:*" "@snort/system-react": "workspace:*" "@szhsin/react-menu": ^3.3.1 "@types/debug": ^4.1.8 @@ -2756,6 +2757,7 @@ __metadata: react-twitter-embed: ^4.0.4 source-map-loader: ^4.0.1 terser-webpack-plugin: ^5.3.9 + tinybench: ^2.5.0 ts-jest: ^29.1.0 ts-loader: ^9.4.4 typescript: ^5.2.2 @@ -2822,6 +2824,12 @@ __metadata: languageName: unknown linkType: soft +"@snort/system-query@workspace:*, @snort/system-query@workspace:packages/system-query": + version: 0.0.0-use.local + resolution: "@snort/system-query@workspace:packages/system-query" + languageName: unknown + linkType: soft + "@snort/system-react@workspace:*, @snort/system-react@workspace:packages/system-react": version: 0.0.0-use.local resolution: "@snort/system-react@workspace:packages/system-react" @@ -2844,6 +2852,7 @@ __metadata: "@peculiar/webcrypto": ^1.4.3 "@scure/base": ^1.1.2 "@snort/shared": ^1.0.5 + "@snort/system-query": "workspace:*" "@stablelib/xchacha20": ^1.0.1 "@types/debug": ^4.1.8 "@types/jest": ^29.5.1 @@ -13153,6 +13162,13 @@ __metadata: languageName: node linkType: hard +"tinybench@npm:^2.5.0": + version: 2.5.0 + resolution: "tinybench@npm:2.5.0" + checksum: 284bb9428f197ec8b869c543181315e65e41ccfdad3c4b6c916bb1fdae1b5c6785661b0d90cf135b48d833b03cb84dc5357b2d33ec65a1f5971fae0ab2023821 + languageName: node + linkType: hard + "tmp@npm:^0.0.33": version: 0.0.33 resolution: "tmp@npm:0.0.33"