feat: upgrade caches to worker
This commit is contained in:
parent
3c808688f8
commit
aa58ec4185
@ -23,6 +23,7 @@
|
||||
"debug": "^4.3.4",
|
||||
"dexie": "^3.2.4",
|
||||
"emojilib": "^3.0.10",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"fuse.js": "^7.0.0",
|
||||
"highlight.js": "^11.8.0",
|
||||
"light-bolt11-decoder": "^2.1.0",
|
||||
|
@ -2,11 +2,7 @@ import { RelayMetricCache, UserProfileCache, UserRelaysCache } from "@snort/syst
|
||||
import { SnortSystemDb } from "@snort/system-web";
|
||||
|
||||
import { ChatCache } from "./ChatCache";
|
||||
import { EventInteractionCache } from "./EventInteractionCache";
|
||||
import { FollowListCache } from "./FollowListCache";
|
||||
import { FollowsFeedCache } from "./FollowsFeed";
|
||||
import { GiftWrapCache } from "./GiftWrapCache";
|
||||
import { NotificationsCache } from "./Notifications";
|
||||
import { Payments } from "./PaymentsCache";
|
||||
|
||||
export const SystemDb = new SnortSystemDb();
|
||||
@ -16,23 +12,15 @@ export const RelayMetrics = new RelayMetricCache(SystemDb.relayMetrics);
|
||||
|
||||
export const Chats = new ChatCache();
|
||||
export const PaymentsCache = new Payments();
|
||||
export const InteractionCache = new EventInteractionCache();
|
||||
export const GiftsCache = new GiftWrapCache();
|
||||
export const Notifications = new NotificationsCache();
|
||||
export const FollowsFeed = new FollowsFeedCache();
|
||||
export const FollowLists = new FollowListCache();
|
||||
|
||||
export async function preload(follows?: Array<string>) {
|
||||
const preloads = [
|
||||
UserCache.preload(follows),
|
||||
Chats.preload(),
|
||||
InteractionCache.preload(),
|
||||
UserRelays.preload(follows),
|
||||
RelayMetrics.preload(),
|
||||
GiftsCache.preload(),
|
||||
Notifications.preload(),
|
||||
FollowsFeed.preload(),
|
||||
FollowLists.preload(),
|
||||
UserRelays.preload(follows),
|
||||
];
|
||||
await Promise.all(preloads);
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ import { ZapperQueue } from "@/Components/Event/Note/NoteFooter/ZapperQueue";
|
||||
import { ZapsSummary } from "@/Components/Event/ZapsSummary";
|
||||
import ZapModal from "@/Components/ZapModal/ZapModal";
|
||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||
import { useInteractionCache } from "@/Hooks/useInteractionCache";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import { getDisplayName } from "@/Utils";
|
||||
import { Zapper, ZapTarget } from "@/Utils/Zapper";
|
||||
@ -35,10 +34,9 @@ export const FooterZapButton = ({ ev, zaps, onClickZappers }: ZapIconProps) => {
|
||||
}));
|
||||
const walletState = useWallet();
|
||||
const wallet = walletState.wallet;
|
||||
const interactionCache = useInteractionCache(publicKey, ev.id);
|
||||
const link = NostrLink.fromEvent(ev);
|
||||
const zapTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
|
||||
const didZap = interactionCache.data.zapped || zaps.some(a => a.sender === publicKey);
|
||||
const didZap = zaps.some(a => a.sender === publicKey);
|
||||
const [showZapModal, setShowZapModal] = useState(false);
|
||||
const { formatMessage } = useIntl();
|
||||
const [zapping, setZapping] = useState(false);
|
||||
@ -102,7 +100,6 @@ export const FooterZapButton = ({ ev, zaps, onClickZappers }: ZapIconProps) => {
|
||||
if (CONFIG.features.zapPool) {
|
||||
ZapPoolController?.allocate(totalSent);
|
||||
}
|
||||
await interactionCache.zap();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -143,7 +140,7 @@ export const FooterZapButton = ({ ev, zaps, onClickZappers }: ZapIconProps) => {
|
||||
value={zapTotal}
|
||||
onClick={fastZap}
|
||||
/>
|
||||
<ZapsSummary zaps={zaps} onClick={onClickZappers} />
|
||||
<ZapsSummary zaps={zaps} onClick={onClickZappers ?? (() => { })} />
|
||||
</div>
|
||||
{showZapModal && (
|
||||
<ZapModal
|
||||
|
@ -5,7 +5,6 @@ import { useIntl } from "react-intl";
|
||||
|
||||
import { AsyncFooterIcon } from "@/Components/Event/Note/NoteFooter/AsyncFooterIcon";
|
||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||
import { useInteractionCache } from "@/Hooks/useInteractionCache";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
|
||||
export const LikeButton = ({
|
||||
@ -17,12 +16,10 @@ export const LikeButton = ({
|
||||
}) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { publicKey } = useLogin(s => ({ publicKey: s.publicKey }));
|
||||
const interactionCache = useInteractionCache(publicKey, ev.id);
|
||||
const { publisher, system } = useEventPublisher();
|
||||
|
||||
const hasReacted = (emoji: string) => {
|
||||
return (
|
||||
interactionCache.data.reacted ||
|
||||
positiveReactions?.some(({ pubkey, content }) => normalizeReaction(content) === emoji && pubkey === publicKey)
|
||||
);
|
||||
};
|
||||
@ -31,7 +28,6 @@ export const LikeButton = ({
|
||||
if (!hasReacted(content) && publisher) {
|
||||
const evLike = await publisher.react(ev, content);
|
||||
system.BroadcastEvent(evLike);
|
||||
interactionCache.react();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||
import { useEventReactions, useReactions } from "@snort/system-react";
|
||||
import { useEventReactions } from "@snort/system-react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
|
||||
import { FooterZapButton } from "@/Components/Event/Note/NoteFooter/FooterZapButton";
|
||||
@ -8,6 +8,7 @@ import { PowIcon } from "@/Components/Event/Note/NoteFooter/PowIcon";
|
||||
import { ReplyButton } from "@/Components/Event/Note/NoteFooter/ReplyButton";
|
||||
import { RepostButton } from "@/Components/Event/Note/NoteFooter/RepostButton";
|
||||
import ReactionsModal from "@/Components/Event/Note/ReactionsModal";
|
||||
import { useReactionsView } from "@/Feed/WorkerRelayView";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
|
||||
export interface NoteFooterProps {
|
||||
@ -21,7 +22,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
const ids = useMemo(() => [link], [link]);
|
||||
const [showReactions, setShowReactions] = useState(false);
|
||||
|
||||
const related = useReactions("note:reactions", ids, undefined, false);
|
||||
const related = useReactionsView(ids, false);
|
||||
const { reactions, zaps, reposts } = useEventReactions(link, related);
|
||||
const { positive } = reactions;
|
||||
|
||||
|
@ -7,7 +7,6 @@ import { AsyncFooterIcon } from "@/Components/Event/Note/NoteFooter/AsyncFooterI
|
||||
import Icon from "@/Components/Icons/Icon";
|
||||
import messages from "@/Components/messages";
|
||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||
import { useInteractionCache } from "@/Hooks/useInteractionCache";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import { useNoteCreator } from "@/State/NoteCreator";
|
||||
|
||||
@ -18,11 +17,10 @@ export const RepostButton = ({ ev, reposts }: { ev: TaggedNostrEvent; reposts: T
|
||||
preferences: s.appData.item.preferences,
|
||||
publicKey: s.publicKey,
|
||||
}));
|
||||
const interactionCache = useInteractionCache(publicKey, ev.id);
|
||||
const note = useNoteCreator(n => ({ show: n.show, replyTo: n.replyTo, update: n.update, quote: n.quote }));
|
||||
|
||||
const hasReposted = () => {
|
||||
return interactionCache.data.reposted || reposts.some(a => a.pubkey === publicKey);
|
||||
return reposts.some(a => a.pubkey === publicKey);
|
||||
};
|
||||
|
||||
const repost = async () => {
|
||||
@ -30,7 +28,6 @@ export const RepostButton = ({ ev, reposts }: { ev: TaggedNostrEvent; reposts: T
|
||||
if (!prefs.confirmReposts || window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.id }))) {
|
||||
const evRepost = await publisher.repost(ev);
|
||||
system.BroadcastEvent(evRepost);
|
||||
await interactionCache.repost();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { ReactNode, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import React, { ReactNode, useCallback, useMemo, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
export interface NoteTimeProps {
|
||||
@ -38,7 +38,7 @@ const NoteTime: React.FC<NoteTimeProps> = ({ from, fallback }) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [time, setTime] = useState<string | ReactNode>(calcTime(from));
|
||||
const [time] = useState<string | ReactNode>(calcTime(from));
|
||||
|
||||
const absoluteTime = useMemo(
|
||||
() =>
|
||||
@ -51,15 +51,6 @@ const NoteTime: React.FC<NoteTimeProps> = ({ from, fallback }) => {
|
||||
|
||||
const isoDate = useMemo(() => new Date(from).toISOString(), [from]);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => {
|
||||
const newTime = calcTime(from);
|
||||
setTime(s => (s !== newTime ? newTime : s));
|
||||
}, 60_000); // update every minute
|
||||
|
||||
return () => clearInterval(t);
|
||||
}, [from]);
|
||||
|
||||
return (
|
||||
<time dateTime={isoDate} title={absoluteTime}>
|
||||
{time || fallback}
|
||||
|
@ -2,16 +2,15 @@ import "./Timeline.css";
|
||||
|
||||
import { unixNow } from "@snort/shared";
|
||||
import { EventKind, NostrEvent, TaggedNostrEvent } from "@snort/system";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
import { ReactNode, useCallback, useContext, useMemo, useState, useSyncExternalStore } from "react";
|
||||
import { ReactNode, useCallback, useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { FollowsFeed } from "@/Cache";
|
||||
import { ShowMoreInView } from "@/Components/Event/ShowMore";
|
||||
import { DisplayAs, DisplayAsSelector } from "@/Components/Feed/DisplayAsSelector";
|
||||
import { TimelineRenderer } from "@/Components/Feed/TimelineRenderer";
|
||||
import { LiveStreams } from "@/Components/LiveStream/LiveStreams";
|
||||
import useHashtagsFeed from "@/Feed/HashtagsFeed";
|
||||
import { useFollowsTimelineView } from "@/Feed/WorkerRelayView";
|
||||
import useHistoryState from "@/Hooks/useHistoryState";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import useModeration from "@/Hooks/useModeration";
|
||||
@ -35,11 +34,8 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
|
||||
const displayAsInitial = props.displayAs ?? login.feedDisplayAs ?? "list";
|
||||
const [displayAs, setDisplayAs] = useState<DisplayAs>(displayAsInitial);
|
||||
const [latest, setLatest] = useHistoryState(unixNow(), "TimelineFollowsLatest");
|
||||
const feed = useSyncExternalStore(
|
||||
cb => FollowsFeed.hook(cb, "*"),
|
||||
() => FollowsFeed.snapshot(),
|
||||
);
|
||||
const system = useContext(SnortContext);
|
||||
const [limit, setLimit] = useState(50);
|
||||
const feed = useFollowsTimelineView(limit);
|
||||
const { muted, isEventMuted } = useModeration();
|
||||
|
||||
const sortedFeed = useMemo(() => orderDescending(feed), [feed]);
|
||||
@ -125,7 +121,9 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
|
||||
displayAs={displayAs}
|
||||
/>
|
||||
{sortedFeed.length > 0 && (
|
||||
<ShowMoreInView onClick={async () => await FollowsFeed.loadMore(system, login, oldest ?? unixNow())} />
|
||||
<ShowMoreInView onClick={() => {
|
||||
setLimit(s => s + 20);
|
||||
}} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { HexKey } from "@snort/system";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { FollowsFeed } from "@/Cache";
|
||||
import AsyncButton from "@/Components/Button/AsyncButton";
|
||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
@ -24,7 +23,6 @@ export default function FollowButton(props: FollowButtonProps) {
|
||||
if (publisher) {
|
||||
const ev = await publisher.contactList([pubkey, ...follows.item].map(a => ["p", a]));
|
||||
system.BroadcastEvent(ev);
|
||||
await FollowsFeed.backFill(system, [pubkey]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,6 @@ import { HexKey } from "@snort/system";
|
||||
import { ReactNode, useMemo } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { FollowsFeed } from "@/Cache";
|
||||
import ProfilePreview from "@/Components/User/ProfilePreview";
|
||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
@ -43,7 +42,6 @@ export default function FollowListBase({
|
||||
const ev = await publisher.contactList(newFollows.map(a => ["p", a]));
|
||||
setFollows(id, newFollows, ev.created_at);
|
||||
await system.BroadcastEvent(ev);
|
||||
await FollowsFeed.backFill(system, pubkeys);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,19 @@
|
||||
import { EventKind, NostrLink, parseRelayTags, RequestBuilder, TaggedNostrEvent } from "@snort/system";
|
||||
import {
|
||||
EventKind,
|
||||
NostrLink,
|
||||
parseRelayTags,
|
||||
RequestBuilder,
|
||||
socialGraphInstance,
|
||||
TaggedNostrEvent,
|
||||
} from "@snort/system";
|
||||
import { useRequestBuilder } from "@snort/system-react";
|
||||
import { usePrevious } from "@uidotdev/usehooks";
|
||||
import { useEffect, useMemo } from "react";
|
||||
|
||||
import { FollowLists, FollowsFeed, GiftsCache, Notifications } from "@/Cache";
|
||||
import { Nip4Chats, Nip28Chats } from "@/chat";
|
||||
import { Nip28ChatSystem } from "@/chat/nip28";
|
||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import { useRefreshFeedCache } from "@/Hooks/useRefreshFeedcache";
|
||||
import { bech32ToHex, debounce, getNewest, getNewestEventTagsByKey, unwrap } from "@/Utils";
|
||||
import { SnortPubKey } from "@/Utils/Const";
|
||||
import {
|
||||
@ -26,6 +31,8 @@ import {
|
||||
} from "@/Utils/Login";
|
||||
import { SubscriptionEvent } from "@/Utils/Subscription";
|
||||
|
||||
import { useFollowsContactListView } from "./WorkerRelayView";
|
||||
|
||||
/**
|
||||
* Managed loading data for the current logged in user
|
||||
*/
|
||||
@ -34,10 +41,10 @@ export default function useLoginFeed() {
|
||||
const { publicKey: pubKey, follows } = login;
|
||||
const { publisher, system } = useEventPublisher();
|
||||
|
||||
useRefreshFeedCache(Notifications, true);
|
||||
useRefreshFeedCache(FollowsFeed, true);
|
||||
useRefreshFeedCache(GiftsCache, true);
|
||||
useRefreshFeedCache(FollowLists, false);
|
||||
const followLists = useFollowsContactListView();
|
||||
useEffect(() => {
|
||||
followLists.forEach(e => socialGraphInstance.handleEvent(e));
|
||||
}, followLists);
|
||||
|
||||
useEffect(() => {
|
||||
system.checkSigs = login.appData.item.preferences.checkSigs;
|
||||
@ -106,7 +113,8 @@ export default function useLoginFeed() {
|
||||
const pTags = contactList.tags.filter(a => a[0] === "p").map(a => a[1]);
|
||||
setFollows(login.id, pTags, contactList.created_at * 1000);
|
||||
|
||||
FollowsFeed.backFillIfMissing(system, pTags);
|
||||
// TODO: fixup
|
||||
// FollowsFeed.backFillIfMissing(system, pTags);
|
||||
}
|
||||
|
||||
const relays = getNewest(loginFeed.filter(a => a.kind === EventKind.Relays));
|
||||
|
164
packages/app/src/Feed/WorkerRelayView.ts
Normal file
164
packages/app/src/Feed/WorkerRelayView.ts
Normal file
@ -0,0 +1,164 @@
|
||||
import { unixNow } from "@snort/shared";
|
||||
import { EventKind, NostrEvent, NostrLink, ReqFilter, RequestBuilder, TaggedNostrEvent } from "@snort/system";
|
||||
import { useRequestBuilder } from "@snort/system-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import { Relay } from "@/system";
|
||||
import { Day } from "@/Utils/Const";
|
||||
|
||||
export function useWorkerRelayView(id: string, filters: Array<ReqFilter>, maxWindow?: number) {
|
||||
const [events, setEvents] = useState<Array<NostrEvent>>([]);
|
||||
const [rb, setRb] = useState<RequestBuilder>();
|
||||
useRequestBuilder(rb);
|
||||
|
||||
useEffect(() => {
|
||||
Relay.req([
|
||||
"REQ",
|
||||
`${id}+latest`,
|
||||
...filters.map(f => ({
|
||||
...f,
|
||||
until: undefined,
|
||||
since: undefined,
|
||||
limit: 1,
|
||||
})),
|
||||
]).then(latest => {
|
||||
const rb = new RequestBuilder(id);
|
||||
filters
|
||||
.map((f, i) => ({
|
||||
...f,
|
||||
limit: undefined,
|
||||
until: undefined,
|
||||
since: latest?.at(i)?.created_at ?? (maxWindow ? unixNow() - maxWindow : undefined),
|
||||
}))
|
||||
.forEach(f => rb.withBareFilter(f));
|
||||
setRb(rb);
|
||||
});
|
||||
Relay.req(["REQ", id, ...filters]).then(setEvents);
|
||||
}, [id, filters, maxWindow]);
|
||||
|
||||
return events as Array<TaggedNostrEvent>;
|
||||
}
|
||||
|
||||
export function useWorkerRelayViewCount(id: string, filters: Array<ReqFilter>, maxWindow?: number) {
|
||||
const [count, setCount] = useState(0);
|
||||
const [rb, setRb] = useState<RequestBuilder>();
|
||||
useRequestBuilder(rb);
|
||||
|
||||
useEffect(() => {
|
||||
Relay.req([
|
||||
"REQ",
|
||||
`${id}+latest`,
|
||||
...filters.map(f => ({
|
||||
...f,
|
||||
until: undefined,
|
||||
since: undefined,
|
||||
limit: 1,
|
||||
})),
|
||||
]).then(latest => {
|
||||
const rb = new RequestBuilder(id);
|
||||
filters
|
||||
.map((f, i) => ({
|
||||
...f,
|
||||
limit: undefined,
|
||||
until: undefined,
|
||||
since: latest?.at(i)?.created_at ?? (maxWindow ? unixNow() - maxWindow : undefined),
|
||||
}))
|
||||
.forEach(f => rb.withBareFilter(f));
|
||||
setRb(rb);
|
||||
});
|
||||
Relay.count(["REQ", id, ...filters]).then(setCount);
|
||||
}, [id, filters, maxWindow]);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
export function useFollowsTimelineView(limit = 20) {
|
||||
const follows = useLogin(s => s.follows.item);
|
||||
const kinds = [EventKind.TextNote, EventKind.Repost, EventKind.Polls];
|
||||
|
||||
const filter = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
authors: follows,
|
||||
kinds,
|
||||
limit,
|
||||
},
|
||||
];
|
||||
}, [follows, limit]);
|
||||
return useWorkerRelayView("follows-timeline", filter, Day * 7);
|
||||
}
|
||||
|
||||
export function useNotificationsView() {
|
||||
const publicKey = useLogin(s => s.publicKey);
|
||||
const kinds = [EventKind.TextNote, EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt];
|
||||
const req = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
"#p": [publicKey ?? ""],
|
||||
kinds,
|
||||
since: unixNow() - Day * 7,
|
||||
},
|
||||
];
|
||||
}, [publicKey]);
|
||||
return useWorkerRelayView("notifications", req, Day * 30);
|
||||
}
|
||||
|
||||
export function useReactionsView(ids: Array<NostrLink>, leaveOpen = true) {
|
||||
const req = useMemo(() => {
|
||||
const rb = new RequestBuilder("reactions");
|
||||
rb.withOptions({ leaveOpen });
|
||||
const grouped = ids.reduce(
|
||||
(acc, v) => {
|
||||
acc[v.type] ??= [];
|
||||
acc[v.type].push(v);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, Array<NostrLink>>,
|
||||
);
|
||||
|
||||
for (const [, v] of Object.entries(grouped)) {
|
||||
rb.withFilter().kinds([EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt]).replyToLink(v);
|
||||
}
|
||||
return rb.buildRaw();
|
||||
}, [ids]);
|
||||
|
||||
return useWorkerRelayView("reactions", req, undefined);
|
||||
}
|
||||
|
||||
export function useReactionsViewCount(ids: Array<NostrLink>, leaveOpen = true) {
|
||||
const req = useMemo(() => {
|
||||
const rb = new RequestBuilder("reactions");
|
||||
rb.withOptions({ leaveOpen });
|
||||
const grouped = ids.reduce(
|
||||
(acc, v) => {
|
||||
acc[v.type] ??= [];
|
||||
acc[v.type].push(v);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, Array<NostrLink>>,
|
||||
);
|
||||
|
||||
for (const [, v] of Object.entries(grouped)) {
|
||||
rb.withFilter().kinds([EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt]).replyToLink(v);
|
||||
}
|
||||
return rb.buildRaw();
|
||||
}, [ids]);
|
||||
|
||||
return useWorkerRelayViewCount("reactions", req, undefined);
|
||||
}
|
||||
|
||||
export function useFollowsContactListView() {
|
||||
const follows = useLogin(s => s.follows.item);
|
||||
const kinds = [EventKind.ContactList, EventKind.Relays];
|
||||
|
||||
const filter = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
authors: follows,
|
||||
kinds,
|
||||
},
|
||||
];
|
||||
}, [follows]);
|
||||
return useWorkerRelayView("follows-contacts-relays", filter, undefined);
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
import { unwrap } from "@snort/shared";
|
||||
import { RequestBuilder, TaggedNostrEvent } from "@snort/system";
|
||||
import { useEffect, useMemo } from "react";
|
||||
|
||||
import { RefreshFeedCache } from "@/Cache/RefreshFeedCache";
|
||||
|
||||
import useEventPublisher from "./useEventPublisher";
|
||||
import useLogin from "./useLogin";
|
||||
|
||||
export function useRefreshFeedCache<T>(c: RefreshFeedCache<T>, leaveOpen = false) {
|
||||
const login = useLogin();
|
||||
const { publisher, system } = useEventPublisher();
|
||||
|
||||
const sub = useMemo(() => {
|
||||
if (login.publicKey) {
|
||||
const rb = new RequestBuilder(`using-${c.name}`);
|
||||
rb.withOptions({
|
||||
leaveOpen,
|
||||
});
|
||||
c.buildSub(login, rb);
|
||||
return rb;
|
||||
}
|
||||
return undefined;
|
||||
}, [login]);
|
||||
|
||||
useEffect(() => {
|
||||
if (sub) {
|
||||
const q = system.Query(sub);
|
||||
const handler = (evs: Array<TaggedNostrEvent>) => {
|
||||
c.onEvent(evs, unwrap(login.publicKey), publisher);
|
||||
};
|
||||
q.on("event", handler);
|
||||
q.uncancel();
|
||||
return () => {
|
||||
q.off("event", handler);
|
||||
q.cancel();
|
||||
};
|
||||
}
|
||||
}, [sub]);
|
||||
}
|
@ -1,14 +1,11 @@
|
||||
import { useMemo, useSyncExternalStore } from "react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { Notifications } from "@/Cache";
|
||||
import { useNotificationsView } from "@/Feed/WorkerRelayView";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
|
||||
export function HasNotificationsMarker() {
|
||||
const readNotifications = useLogin(s => s.readNotifications);
|
||||
const notifications = useSyncExternalStore(
|
||||
c => Notifications.hook(c, "*"),
|
||||
() => Notifications.snapshot(),
|
||||
);
|
||||
const notifications = useNotificationsView();
|
||||
const latestNotification = useMemo(
|
||||
() => notifications.reduce((acc, v) => (v.created_at > acc ? v.created_at : acc), 0),
|
||||
[notifications],
|
||||
|
146
packages/app/src/Pages/Notifications/NotificationGroup.tsx
Normal file
146
packages/app/src/Pages/Notifications/NotificationGroup.tsx
Normal file
@ -0,0 +1,146 @@
|
||||
import { unwrap } from "@snort/shared";
|
||||
import { EventKind, NostrLink, parseZap, TaggedNostrEvent } from "@snort/system";
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
import { useMemo } from "react";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import Icon from "@/Components/Icons/Icon";
|
||||
import ProfileImage from "@/Components/User/ProfileImage";
|
||||
import { dedupe, getDisplayName } from "@/Utils";
|
||||
import { formatShort } from "@/Utils/Number";
|
||||
|
||||
import { notificationContext } from "./notificationContext";
|
||||
import { NotificationContext } from "./Notifications";
|
||||
|
||||
export function NotificationGroup({ evs, onClick }: { evs: Array<TaggedNostrEvent>; onClick?: (link: NostrLink) => void; }) {
|
||||
const { ref, inView } = useInView({ triggerOnce: true });
|
||||
const { formatMessage } = useIntl();
|
||||
const kind = evs[0].kind;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const zaps = useMemo(() => {
|
||||
return evs.filter(a => a.kind === EventKind.ZapReceipt).map(a => parseZap(a));
|
||||
}, [evs]);
|
||||
const pubkeys = dedupe(
|
||||
evs.map(a => {
|
||||
if (a.kind === EventKind.ZapReceipt) {
|
||||
const zap = unwrap(zaps.find(b => b.id === a.id));
|
||||
return zap.anonZap ? "anon" : zap.sender ?? a.pubkey;
|
||||
}
|
||||
return a.pubkey;
|
||||
})
|
||||
);
|
||||
const firstPubkey = pubkeys[0];
|
||||
const firstPubkeyProfile = useUserProfile(inView ? (firstPubkey === "anon" ? "" : firstPubkey) : "");
|
||||
const context = notificationContext(evs[0]);
|
||||
const totalZaps = zaps.reduce((acc, v) => acc + v.amount, 0);
|
||||
|
||||
const iconName = () => {
|
||||
switch (kind) {
|
||||
case EventKind.Reaction:
|
||||
return "heart-solid";
|
||||
case EventKind.ZapReceipt:
|
||||
return "zap-solid";
|
||||
case EventKind.Repost:
|
||||
return "repeat";
|
||||
case EventKind.TextNote:
|
||||
return "reverse-left";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const actionName = (n: number, name: string) => {
|
||||
switch (kind) {
|
||||
case EventKind.TextNote: {
|
||||
return "";
|
||||
}
|
||||
case EventKind.Reaction: {
|
||||
return (
|
||||
<FormattedMessage
|
||||
defaultMessage="{n,plural,=0{{name} liked} other{{name} & {n} others liked}}"
|
||||
id="kuPHYE"
|
||||
values={{
|
||||
n,
|
||||
name,
|
||||
}} />
|
||||
);
|
||||
}
|
||||
case EventKind.Repost: {
|
||||
return (
|
||||
<FormattedMessage
|
||||
defaultMessage="{n,plural,=0{{name} reposted} other{{name} & {n} others reposted}}"
|
||||
id="kJYo0u"
|
||||
values={{
|
||||
n,
|
||||
name,
|
||||
}} />
|
||||
);
|
||||
}
|
||||
case EventKind.ZapReceipt: {
|
||||
return (
|
||||
<FormattedMessage
|
||||
defaultMessage="{n,plural,=0{{name} zapped} other{{name} & {n} others zapped}}"
|
||||
id="Lw+I+J"
|
||||
values={{
|
||||
n,
|
||||
name,
|
||||
}} />
|
||||
);
|
||||
}
|
||||
}
|
||||
return `${kind}'d your post`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card notification-group" ref={ref}>
|
||||
{inView && (
|
||||
<>
|
||||
<div className="flex flex-col g12">
|
||||
<div>
|
||||
<Icon name={iconName()} size={24} className={iconName()} />
|
||||
</div>
|
||||
<div>{kind === EventKind.ZapReceipt && formatShort(totalZaps)}</div>
|
||||
</div>
|
||||
<div className="flex flex-col w-max g12">
|
||||
<div className="flex">
|
||||
{pubkeys
|
||||
.filter(a => a !== "anon")
|
||||
.slice(0, 12)
|
||||
.map(v => (
|
||||
<ProfileImage
|
||||
key={v}
|
||||
showUsername={kind === EventKind.TextNote}
|
||||
pubkey={v}
|
||||
size={40}
|
||||
overrideUsername={v === "" ? formatMessage({ defaultMessage: "Anon", id: "bfvyfs" }) : undefined} />
|
||||
))}
|
||||
</div>
|
||||
{kind !== EventKind.TextNote && (
|
||||
<div className="names">
|
||||
{actionName(
|
||||
pubkeys.length - 1,
|
||||
firstPubkey === "anon"
|
||||
? formatMessage({ defaultMessage: "Anon", id: "bfvyfs" })
|
||||
: getDisplayName(firstPubkeyProfile, firstPubkey)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{context && (
|
||||
<NotificationContext
|
||||
link={context}
|
||||
onClick={() => {
|
||||
if (onClick) {
|
||||
onClick(context);
|
||||
} else {
|
||||
navigate(`/${context.encode(CONFIG.eventLinkPrefix)}`);
|
||||
}
|
||||
}} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,75 +1,35 @@
|
||||
import "./Notifications.css";
|
||||
|
||||
import { unwrap } from "@snort/shared";
|
||||
import { EventExt, EventKind, NostrEvent, NostrLink, NostrPrefix, parseZap, TaggedNostrEvent } from "@snort/system";
|
||||
import { useEventFeed, useUserProfile } from "@snort/system-react";
|
||||
import { lazy, Suspense, useEffect, useMemo, useState, useSyncExternalStore } from "react";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { EventKind, NostrEvent, NostrLink, NostrPrefix, TaggedNostrEvent } from "@snort/system";
|
||||
import { useEventFeed } from "@snort/system-react";
|
||||
import { lazy, Suspense, useEffect, useMemo } from "react";
|
||||
|
||||
import { Notifications } from "@/Cache";
|
||||
import { ShowMoreInView } from "@/Components/Event/ShowMore";
|
||||
import Icon from "@/Components/Icons/Icon";
|
||||
import { LiveEvent } from "@/Components/LiveStream/LiveEvent";
|
||||
import PageSpinner from "@/Components/PageSpinner";
|
||||
import Text from "@/Components/Text/Text";
|
||||
import ProfileImage from "@/Components/User/ProfileImage";
|
||||
import ProfilePreview from "@/Components/User/ProfilePreview";
|
||||
import { useNotificationsView } from "@/Feed/WorkerRelayView";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import useModeration from "@/Hooks/useModeration";
|
||||
import { dedupe, getDisplayName, orderDescending } from "@/Utils";
|
||||
import { orderDescending } from "@/Utils";
|
||||
import { markNotificationsRead } from "@/Utils/Login";
|
||||
import { formatShort } from "@/Utils/Number";
|
||||
const NotificationGraph = lazy(() => import("@/Pages/Notifications/NotificationChart"));
|
||||
|
||||
function notificationContext(ev: TaggedNostrEvent) {
|
||||
switch (ev.kind) {
|
||||
case EventKind.ZapReceipt: {
|
||||
const aTag = ev.tags.find(a => a[0] === "a");
|
||||
if (aTag) {
|
||||
return NostrLink.fromTag(aTag);
|
||||
}
|
||||
const eTag = ev.tags.find(a => a[0] === "e");
|
||||
if (eTag) {
|
||||
return NostrLink.fromTag(eTag);
|
||||
}
|
||||
const pTag = ev.tags.find(a => a[0] === "p");
|
||||
if (pTag) {
|
||||
return NostrLink.fromTag(pTag);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EventKind.Repost:
|
||||
case EventKind.Reaction: {
|
||||
const thread = EventExt.extractThread(ev);
|
||||
const tag = unwrap(thread?.replyTo ?? thread?.root ?? { value: ev.id, key: "e" });
|
||||
if (tag.key === "e" || tag.key === "a") {
|
||||
return NostrLink.fromThreadTag(tag);
|
||||
} else {
|
||||
throw new Error("Unknown thread context");
|
||||
}
|
||||
}
|
||||
case EventKind.TextNote: {
|
||||
return NostrLink.fromEvent(ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
import { notificationContext } from "./notificationContext";
|
||||
import { NotificationGroup } from "./NotificationGroup";
|
||||
const NotificationGraph = lazy(() => import("@/Pages/Notifications/NotificationChart"));
|
||||
|
||||
export default function NotificationsPage({ onClick }: { onClick?: (link: NostrLink) => void }) {
|
||||
const login = useLogin();
|
||||
const { isMuted } = useModeration();
|
||||
const groupInterval = 3600 * 3;
|
||||
const [showN, setShowN] = useState(10);
|
||||
const groupInterval = 3600 * 6;
|
||||
|
||||
useEffect(() => {
|
||||
markNotificationsRead(login);
|
||||
}, []);
|
||||
|
||||
const notifications = useSyncExternalStore(
|
||||
c => Notifications.hook(c, "*"),
|
||||
() => Notifications.snapshot(),
|
||||
);
|
||||
const notifications = useNotificationsView();
|
||||
|
||||
const timeKey = (ev: NostrEvent) => {
|
||||
const onHour = ev.created_at - (ev.created_at % groupInterval);
|
||||
@ -84,9 +44,8 @@ export default function NotificationsPage({ onClick }: { onClick?: (link: NostrL
|
||||
|
||||
const timeGrouped = useMemo(() => {
|
||||
return myNotifications.reduce((acc, v) => {
|
||||
const key = `${timeKey(v)}:${notificationContext(v as TaggedNostrEvent)?.encode(CONFIG.eventLinkPrefix)}:${
|
||||
v.kind
|
||||
}`;
|
||||
const key = `${timeKey(v)}:${notificationContext(v as TaggedNostrEvent)?.encode(CONFIG.eventLinkPrefix)}:${v.kind
|
||||
}`;
|
||||
if (acc.has(key)) {
|
||||
unwrap(acc.get(key)).push(v as TaggedNostrEvent);
|
||||
} else {
|
||||
@ -106,152 +65,15 @@ export default function NotificationsPage({ onClick }: { onClick?: (link: NostrL
|
||||
)}
|
||||
{login.publicKey &&
|
||||
[...timeGrouped.entries()]
|
||||
.slice(0, showN)
|
||||
.map(([k, g]) => <NotificationGroup key={k} evs={g} onClick={onClick} />)}
|
||||
|
||||
<ShowMoreInView onClick={() => setShowN(s => Math.min(timeGrouped.size, s + 5))} />
|
||||
<ShowMoreInView onClick={() => {}} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function NotificationGroup({ evs, onClick }: { evs: Array<TaggedNostrEvent>; onClick?: (link: NostrLink) => void }) {
|
||||
const { ref, inView } = useInView({ triggerOnce: true });
|
||||
const { formatMessage } = useIntl();
|
||||
const kind = evs[0].kind;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const zaps = useMemo(() => {
|
||||
return evs.filter(a => a.kind === EventKind.ZapReceipt).map(a => parseZap(a));
|
||||
}, [evs]);
|
||||
const pubkeys = dedupe(
|
||||
evs.map(a => {
|
||||
if (a.kind === EventKind.ZapReceipt) {
|
||||
const zap = unwrap(zaps.find(b => b.id === a.id));
|
||||
return zap.anonZap ? "anon" : zap.sender ?? a.pubkey;
|
||||
}
|
||||
return a.pubkey;
|
||||
}),
|
||||
);
|
||||
const firstPubkey = pubkeys[0];
|
||||
const firstPubkeyProfile = useUserProfile(inView ? (firstPubkey === "anon" ? "" : firstPubkey) : "");
|
||||
const context = notificationContext(evs[0]);
|
||||
const totalZaps = zaps.reduce((acc, v) => acc + v.amount, 0);
|
||||
|
||||
const iconName = () => {
|
||||
switch (kind) {
|
||||
case EventKind.Reaction:
|
||||
return "heart-solid";
|
||||
case EventKind.ZapReceipt:
|
||||
return "zap-solid";
|
||||
case EventKind.Repost:
|
||||
return "repeat";
|
||||
case EventKind.TextNote:
|
||||
return "reverse-left";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const actionName = (n: number, name: string) => {
|
||||
switch (kind) {
|
||||
case EventKind.TextNote: {
|
||||
return "";
|
||||
}
|
||||
case EventKind.Reaction: {
|
||||
return (
|
||||
<FormattedMessage
|
||||
defaultMessage="{n,plural,=0{{name} liked} other{{name} & {n} others liked}}"
|
||||
id="kuPHYE"
|
||||
values={{
|
||||
n,
|
||||
name,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case EventKind.Repost: {
|
||||
return (
|
||||
<FormattedMessage
|
||||
defaultMessage="{n,plural,=0{{name} reposted} other{{name} & {n} others reposted}}"
|
||||
id="kJYo0u"
|
||||
values={{
|
||||
n,
|
||||
name,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case EventKind.ZapReceipt: {
|
||||
return (
|
||||
<FormattedMessage
|
||||
defaultMessage="{n,plural,=0{{name} zapped} other{{name} & {n} others zapped}}"
|
||||
id="Lw+I+J"
|
||||
values={{
|
||||
n,
|
||||
name,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return `${kind}'d your post`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card notification-group" ref={ref}>
|
||||
{inView && (
|
||||
<>
|
||||
<div className="flex flex-col g12">
|
||||
<div>
|
||||
<Icon name={iconName()} size={24} className={iconName()} />
|
||||
</div>
|
||||
<div>{kind === EventKind.ZapReceipt && formatShort(totalZaps)}</div>
|
||||
</div>
|
||||
<div className="flex flex-col w-max g12">
|
||||
<div className="flex">
|
||||
{pubkeys
|
||||
.filter(a => a !== "anon")
|
||||
.slice(0, 12)
|
||||
.map(v => (
|
||||
<ProfileImage
|
||||
key={v}
|
||||
showUsername={kind === EventKind.TextNote}
|
||||
pubkey={v}
|
||||
size={40}
|
||||
overrideUsername={v === "" ? formatMessage({ defaultMessage: "Anon", id: "bfvyfs" }) : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{kind !== EventKind.TextNote && (
|
||||
<div className="names">
|
||||
{actionName(
|
||||
pubkeys.length - 1,
|
||||
firstPubkey === "anon"
|
||||
? formatMessage({ defaultMessage: "Anon", id: "bfvyfs" })
|
||||
: getDisplayName(firstPubkeyProfile, firstPubkey),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{context && (
|
||||
<NotificationContext
|
||||
link={context}
|
||||
onClick={() => {
|
||||
if (onClick) {
|
||||
onClick(context);
|
||||
} else {
|
||||
navigate(`/${context.encode(CONFIG.eventLinkPrefix)}`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NotificationContext({ link, onClick }: { link: NostrLink; onClick: () => void }) {
|
||||
export function NotificationContext({ link, onClick }: { link: NostrLink; onClick: () => void }) {
|
||||
const ev = useEventFeed(link);
|
||||
if (link.type === NostrPrefix.PublicKey) {
|
||||
return <ProfilePreview pubkey={link.id} actions={<></>} />;
|
||||
|
35
packages/app/src/Pages/Notifications/notificationContext.tsx
Normal file
35
packages/app/src/Pages/Notifications/notificationContext.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { unwrap } from "@snort/shared";
|
||||
import { EventExt, EventKind, NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||
|
||||
export function notificationContext(ev: TaggedNostrEvent) {
|
||||
switch (ev.kind) {
|
||||
case EventKind.ZapReceipt: {
|
||||
const aTag = ev.tags.find(a => a[0] === "a");
|
||||
if (aTag) {
|
||||
return NostrLink.fromTag(aTag);
|
||||
}
|
||||
const eTag = ev.tags.find(a => a[0] === "e");
|
||||
if (eTag) {
|
||||
return NostrLink.fromTag(eTag);
|
||||
}
|
||||
const pTag = ev.tags.find(a => a[0] === "p");
|
||||
if (pTag) {
|
||||
return NostrLink.fromTag(pTag);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EventKind.Repost:
|
||||
case EventKind.Reaction: {
|
||||
const thread = EventExt.extractThread(ev);
|
||||
const tag = unwrap(thread?.replyTo ?? thread?.root ?? { value: ev.id, key: "e" });
|
||||
if (tag.key === "e" || tag.key === "a") {
|
||||
return NostrLink.fromThreadTag(tag);
|
||||
} else {
|
||||
throw new Error("Unknown thread context");
|
||||
}
|
||||
}
|
||||
case EventKind.TextNote: {
|
||||
return NostrLink.fromEvent(ev);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,20 +1,16 @@
|
||||
import { FeedCache } from "@snort/shared";
|
||||
import { ReactNode, useSyncExternalStore } from "react";
|
||||
import { ReactNode, useEffect, useState, useSyncExternalStore } from "react";
|
||||
import { FormattedMessage, FormattedNumber } from "react-intl";
|
||||
|
||||
import {
|
||||
Chats,
|
||||
FollowLists,
|
||||
FollowsFeed,
|
||||
GiftsCache,
|
||||
InteractionCache,
|
||||
Notifications,
|
||||
PaymentsCache,
|
||||
RelayMetrics,
|
||||
UserCache,
|
||||
UserRelays,
|
||||
} from "@/Cache";
|
||||
import AsyncButton from "@/Components/Button/AsyncButton";
|
||||
import { Relay } from "@/system";
|
||||
|
||||
export function CacheSettings() {
|
||||
return (
|
||||
@ -22,16 +18,12 @@ export function CacheSettings() {
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Cache" id="DBiVK1" />
|
||||
</h3>
|
||||
<RelayCacheStats />
|
||||
<CacheDetails cache={UserCache} name={<FormattedMessage defaultMessage="Profiles" id="2zJXeA" />} />
|
||||
<CacheDetails cache={UserRelays} name={<FormattedMessage defaultMessage="Relay Lists" id="tGXF0Q" />} />
|
||||
<CacheDetails cache={Notifications} name={<FormattedMessage defaultMessage="Notifications" id="NAidKb" />} />
|
||||
<CacheDetails cache={FollowsFeed} name={<FormattedMessage defaultMessage="Follows Feed" id="uKqSN+" />} />
|
||||
<CacheDetails cache={Chats} name={<FormattedMessage defaultMessage="Chats" id="ABAQyo" />} />
|
||||
<CacheDetails cache={RelayMetrics} name={<FormattedMessage defaultMessage="Relay Metrics" id="tjpYlr" />} />
|
||||
<CacheDetails cache={PaymentsCache} name={<FormattedMessage defaultMessage="Payments" id="iYc3Ld" />} />
|
||||
<CacheDetails cache={InteractionCache} name={<FormattedMessage defaultMessage="Interactions" id="u+LyXc" />} />
|
||||
<CacheDetails cache={GiftsCache} name={<FormattedMessage defaultMessage="Gift Wraps" id="fjAcWo" />} />
|
||||
<CacheDetails cache={FollowLists} name={<FormattedMessage defaultMessage="Social Graph" id="CzHZoc" />} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -65,3 +57,39 @@ function CacheDetails<T>({ cache, name }: { cache: FeedCache<T>; name: ReactNode
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RelayCacheStats() {
|
||||
const [counts, setCounts] = useState<Record<string, number>>({});
|
||||
|
||||
useEffect(() => {
|
||||
Relay.summary().then(setCounts);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex justify-between br p bg-superdark">
|
||||
<div className="flex flex-col g4">
|
||||
<FormattedMessage defaultMessage="Worker Relay" id="xSoIUU" />
|
||||
{Object.entries(counts).map(([k, v]) => {
|
||||
return <small key={k}>
|
||||
<FormattedMessage
|
||||
defaultMessage="{n} kind {k} events"
|
||||
id="I97cCX"
|
||||
values={{
|
||||
n: <FormattedNumber value={v} />,
|
||||
k: k
|
||||
}}
|
||||
/>
|
||||
</small>
|
||||
})}
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<AsyncButton onClick={() => {
|
||||
|
||||
}}>
|
||||
<FormattedMessage defaultMessage="Clear" id="/GCoTA" />
|
||||
</AsyncButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
UserMetadata,
|
||||
} from "@snort/system";
|
||||
|
||||
import { Chats, FollowsFeed, GiftsCache, Notifications } from "@/Cache";
|
||||
import { Chats, GiftsCache } from "@/Cache";
|
||||
import SnortApi from "@/External/SnortApi";
|
||||
import { bech32ToHex, dedupeById, deleteRefCode, getCountry, sanitizeRelayUrl, unwrap } from "@/Utils";
|
||||
import { Blasters } from "@/Utils/Const";
|
||||
@ -68,8 +68,6 @@ export function updatePreferences(id: string, p: UserPreferences) {
|
||||
export function logout(id: string) {
|
||||
LoginStore.removeSession(id);
|
||||
GiftsCache.clear();
|
||||
Notifications.clear();
|
||||
FollowsFeed.clear();
|
||||
Chats.clear();
|
||||
deleteRefCode();
|
||||
localStorage.clear();
|
||||
|
@ -35,8 +35,8 @@ import SearchPage from "@/Pages/SearchPage";
|
||||
import SettingsRoutes from "@/Pages/settings/Routes";
|
||||
import { SubscribeRoutes } from "@/Pages/subscribe";
|
||||
import ZapPoolPage from "@/Pages/ZapPool";
|
||||
import { System } from "@/system";
|
||||
import { getCountry, storeRefCode, unwrap } from "@/Utils";
|
||||
import { initRelayWorker, System } from "@/system";
|
||||
import { storeRefCode, unwrap } from "@/Utils";
|
||||
import { LoginStore } from "@/Utils/Login";
|
||||
import { hasWasm, wasmInit, WasmPath } from "@/Utils/wasm";
|
||||
import { Wallets } from "@/Wallet";
|
||||
@ -47,10 +47,10 @@ import { WalletReceivePage } from "./Pages/wallet/receive";
|
||||
import { WalletSendPage } from "./Pages/wallet/send";
|
||||
|
||||
async function initSite() {
|
||||
console.debug(getCountry());
|
||||
storeRefCode();
|
||||
if (hasWasm) {
|
||||
await wasmInit(WasmPath);
|
||||
await initRelayWorker();
|
||||
}
|
||||
const login = LoginStore.takeSnapshot();
|
||||
db.ready = await db.isAvailable();
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { removeUndefined, throwIfOffline } from "@snort/shared";
|
||||
import { mapEventToProfile, NostrEvent, NostrSystem, ProfileLoaderService, socialGraphInstance } from "@snort/system";
|
||||
import { WorkerRelayInterface } from "@snort/worker-relay";
|
||||
import WorkerRelayPath from "@snort/worker-relay/dist/worker?worker&url";
|
||||
|
||||
import { RelayMetrics, SystemDb, UserCache, UserRelays } from "@/Cache";
|
||||
import { addCachedMetadataToFuzzySearch, addEventToFuzzySearch } from "@/Db/FuzzySearch";
|
||||
@ -59,6 +61,25 @@ export async function fetchProfile(key: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export const Relay = new WorkerRelayInterface(WorkerRelayPath);
|
||||
let relayInitStarted = false;
|
||||
export async function initRelayWorker() {
|
||||
if (relayInitStarted) return;
|
||||
relayInitStarted = true;
|
||||
try {
|
||||
if (await Relay.init()) {
|
||||
if (await Relay.open()) {
|
||||
await Relay.migrate();
|
||||
System.on("event", async (_, ev) => {
|
||||
await Relay.event(ev);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton user profile loader
|
||||
*/
|
||||
|
@ -10,14 +10,30 @@ export interface KeyedHookFilter {
|
||||
fn: HookFn;
|
||||
}
|
||||
|
||||
export interface FeedCacheEvents {
|
||||
export interface CacheEvents {
|
||||
change: (keys: Array<string>) => void;
|
||||
}
|
||||
|
||||
export type CachedTable<T> = {
|
||||
preload(): Promise<void>;
|
||||
keysOnTable(): Array<string>;
|
||||
getFromCache(key?: string): T | undefined;
|
||||
get(key?: string): Promise<T | undefined>;
|
||||
bulkGet(keys: Array<string>): Promise<Array<T>>;
|
||||
set(obj: T): Promise<void>;
|
||||
bulkSet(obj: Array<T> | Readonly<Array<T>>): Promise<void>;
|
||||
update<TWithCreated extends T & { created: number; loaded: number }>(
|
||||
m: TWithCreated,
|
||||
): Promise<"new" | "refresh" | "updated" | "no_change">;
|
||||
buffer(keys: Array<string>): Promise<Array<string>>;
|
||||
key(of: T): string;
|
||||
snapshot(): Array<T>;
|
||||
} & EventEmitter<CacheEvents>;
|
||||
|
||||
/**
|
||||
* Dexie backed generic hookable store
|
||||
*/
|
||||
export abstract class FeedCache<TCached> extends EventEmitter<FeedCacheEvents> {
|
||||
export abstract class FeedCache<TCached> extends EventEmitter<CacheEvents> implements CachedTable<TCached> {
|
||||
readonly name: string;
|
||||
#snapshot: Array<TCached> = [];
|
||||
protected log: ReturnType<typeof debug>;
|
||||
|
@ -1,10 +1,10 @@
|
||||
import debug from "debug";
|
||||
import { FeedCache, removeUndefined } from "@snort/shared";
|
||||
import { CachedTable, removeUndefined } from "@snort/shared";
|
||||
import { SystemInterface, TaggedNostrEvent, RequestBuilder } from ".";
|
||||
|
||||
export abstract class BackgroundLoader<T extends { loaded: number; created: number }> {
|
||||
#system: SystemInterface;
|
||||
readonly cache: FeedCache<T>;
|
||||
readonly cache: CachedTable<T>;
|
||||
#log = debug(this.name());
|
||||
|
||||
/**
|
||||
@ -17,7 +17,7 @@ export abstract class BackgroundLoader<T extends { loaded: number; created: numb
|
||||
*/
|
||||
loaderFn?: (pubkeys: Array<string>) => Promise<Array<T>>;
|
||||
|
||||
constructor(system: SystemInterface, cache: FeedCache<T>) {
|
||||
constructor(system: SystemInterface, cache: CachedTable<T>) {
|
||||
this.#system = system;
|
||||
this.cache = cache;
|
||||
this.#FetchMetadata();
|
||||
|
@ -5,7 +5,7 @@ import { ProfileLoaderService } from "./profile-cache";
|
||||
import { RelayCache, RelayMetadataLoader } from "./outbox-model";
|
||||
import { Optimizer } from "./query-optimizer";
|
||||
import { base64 } from "@scure/base";
|
||||
import { FeedCache } from "@snort/shared";
|
||||
import { CachedTable } from "@snort/shared";
|
||||
import { ConnectionPool } from "./connection-pool";
|
||||
import EventEmitter from "eventemitter3";
|
||||
import { QueryEvents } from "./query";
|
||||
@ -143,7 +143,7 @@ export interface SystemInterface {
|
||||
/**
|
||||
* Generic cache store for events
|
||||
*/
|
||||
get eventsCache(): FeedCache<NostrEvent>;
|
||||
get eventsCache(): CachedTable<NostrEvent>;
|
||||
|
||||
/**
|
||||
* Relay loader loads relay metadata for a set of profiles
|
||||
|
@ -1,8 +1,8 @@
|
||||
import debug from "debug";
|
||||
import EventEmitter from "eventemitter3";
|
||||
|
||||
import { FeedCache } from "@snort/shared";
|
||||
import { NostrEvent, ReqFilter, TaggedNostrEvent } from "./nostr";
|
||||
import { CachedTable } from "@snort/shared";
|
||||
import { NostrEvent, TaggedNostrEvent } from "./nostr";
|
||||
import { RelaySettings, ConnectionStateSnapshot, OkResponse } from "./connection";
|
||||
import { BuiltRawReqFilter, RequestBuilder } from "./request-builder";
|
||||
import { RelayMetricHandler } from "./relay-metric-handler";
|
||||
@ -17,7 +17,6 @@ import {
|
||||
RelayMetricCache,
|
||||
UsersRelays,
|
||||
SnortSystemDb,
|
||||
EventExt,
|
||||
QueryLike,
|
||||
} from ".";
|
||||
import { EventsCache } from "./cache/events";
|
||||
@ -34,10 +33,10 @@ export interface NostrSystemEvents {
|
||||
}
|
||||
|
||||
export interface NostrsystemProps {
|
||||
relayCache?: FeedCache<UsersRelays>;
|
||||
profileCache?: FeedCache<CachedMetadata>;
|
||||
relayMetrics?: FeedCache<RelayMetrics>;
|
||||
eventsCache?: FeedCache<NostrEvent>;
|
||||
relayCache?: CachedTable<UsersRelays>;
|
||||
profileCache?: CachedTable<CachedMetadata>;
|
||||
relayMetrics?: CachedTable<RelayMetrics>;
|
||||
eventsCache?: CachedTable<NostrEvent>;
|
||||
optimizer?: Optimizer;
|
||||
db?: SnortSystemDb;
|
||||
checkSigs?: boolean;
|
||||
@ -53,17 +52,17 @@ export class NostrSystem extends EventEmitter<NostrSystemEvents> implements Syst
|
||||
/**
|
||||
* Storage class for user relay lists
|
||||
*/
|
||||
readonly relayCache: FeedCache<UsersRelays>;
|
||||
readonly relayCache: CachedTable<UsersRelays>;
|
||||
|
||||
/**
|
||||
* Storage class for user profiles
|
||||
*/
|
||||
readonly profileCache: FeedCache<CachedMetadata>;
|
||||
readonly profileCache: CachedTable<CachedMetadata>;
|
||||
|
||||
/**
|
||||
* Storage class for relay metrics (connects/disconnects)
|
||||
*/
|
||||
readonly relayMetricsCache: FeedCache<RelayMetrics>;
|
||||
readonly relayMetricsCache: CachedTable<RelayMetrics>;
|
||||
|
||||
/**
|
||||
* Profile loading service
|
||||
@ -81,7 +80,7 @@ export class NostrSystem extends EventEmitter<NostrSystemEvents> implements Syst
|
||||
readonly optimizer: Optimizer;
|
||||
|
||||
readonly pool: ConnectionPool;
|
||||
readonly eventsCache: FeedCache<NostrEvent>;
|
||||
readonly eventsCache: CachedTable<NostrEvent>;
|
||||
readonly relayLoader: RelayMetadataLoader;
|
||||
|
||||
/**
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { FeedCache, unixNowMs } from "@snort/shared";
|
||||
import { CachedTable, unixNowMs } from "@snort/shared";
|
||||
import { RelayMetrics } from "./cache";
|
||||
import { TraceReport } from "./query";
|
||||
|
||||
export class RelayMetricHandler {
|
||||
readonly #cache: FeedCache<RelayMetrics>;
|
||||
readonly #cache: CachedTable<RelayMetrics>;
|
||||
|
||||
constructor(cache: FeedCache<RelayMetrics>) {
|
||||
constructor(cache: CachedTable<RelayMetrics>) {
|
||||
this.#cache = cache;
|
||||
|
||||
setInterval(() => {
|
||||
|
@ -21,7 +21,7 @@ import {
|
||||
} from "..";
|
||||
import { NostrSystemEvents, NostrsystemProps } from "../nostr-system";
|
||||
import { WorkerCommand, WorkerMessage } from ".";
|
||||
import { FeedCache } from "@snort/shared";
|
||||
import { CachedTable } from "@snort/shared";
|
||||
import { EventsCache } from "../cache/events";
|
||||
import { RelayMetricHandler } from "../relay-metric-handler";
|
||||
import debug from "debug";
|
||||
@ -31,12 +31,12 @@ export class SystemWorker extends EventEmitter<NostrSystemEvents> implements Sys
|
||||
#log = debug("SystemWorker");
|
||||
#worker: Worker;
|
||||
#commandQueue: Map<string, (v: unknown) => void> = new Map();
|
||||
readonly relayCache: FeedCache<UsersRelays>;
|
||||
readonly profileCache: FeedCache<CachedMetadata>;
|
||||
readonly relayMetricsCache: FeedCache<RelayMetrics>;
|
||||
readonly relayCache: CachedTable<UsersRelays>;
|
||||
readonly profileCache: CachedTable<CachedMetadata>;
|
||||
readonly relayMetricsCache: CachedTable<RelayMetrics>;
|
||||
readonly profileLoader: ProfileLoaderService;
|
||||
readonly relayMetricsHandler: RelayMetricHandler;
|
||||
readonly eventsCache: FeedCache<NostrEvent>;
|
||||
readonly eventsCache: CachedTable<NostrEvent>;
|
||||
readonly relayLoader: RelayMetadataLoader;
|
||||
|
||||
get checkSigs() {
|
||||
|
@ -40,7 +40,15 @@ export class WorkerRelayInterface {
|
||||
return await this.#workerRpc<ReqCommand, Array<NostrEvent>>("req", req);
|
||||
}
|
||||
|
||||
#workerRpc<T, R>(cmd: string, args?: T, timeout = 5_000) {
|
||||
async count(req: ReqCommand) {
|
||||
return await this.#workerRpc<ReqCommand, number>("count", req);
|
||||
}
|
||||
|
||||
async summary() {
|
||||
return await this.#workerRpc<void, Record<string, number>>("summary");
|
||||
}
|
||||
|
||||
#workerRpc<T, R>(cmd: string, args?: T, timeout = 30_000) {
|
||||
const id = uuid();
|
||||
const msg = {
|
||||
id,
|
||||
|
@ -1,16 +1,17 @@
|
||||
import sqlite3InitModule, { Database, Sqlite3Static } from "@sqlite.org/sqlite-wasm";
|
||||
import debug from "debug";
|
||||
import { NostrEvent, ReqFilter } from "types";
|
||||
import { NostrEvent, ReqFilter, unixNowMs } from "./types";
|
||||
|
||||
export class WorkerRelay {
|
||||
#sqlite?: Sqlite3Static;
|
||||
#log = (msg: string) => console.debug(msg);
|
||||
#log = (...msg: Array<any>) => console.debug(...msg);
|
||||
#db?: Database;
|
||||
|
||||
/**
|
||||
* Initialize the SQLite driver
|
||||
*/
|
||||
async init() {
|
||||
if (this.#sqlite) return;
|
||||
this.#sqlite = await sqlite3InitModule();
|
||||
this.#log(`Got SQLite version: ${this.#sqlite.version.libVersion}`);
|
||||
}
|
||||
@ -20,15 +21,26 @@ export class WorkerRelay {
|
||||
*/
|
||||
async open(path: string) {
|
||||
if (!this.#sqlite) throw new Error("Must call init first");
|
||||
if (this.#db) return;
|
||||
|
||||
if ("opfs" in this.#sqlite) {
|
||||
this.#db = new this.#sqlite.oo1.OpfsDb(path, "cw");
|
||||
this.#log(`Opened ${this.#db.filename}`);
|
||||
try {
|
||||
this.#db = new this.#sqlite.oo1.OpfsDb(path, "cw");
|
||||
this.#log(`Opened ${this.#db.filename}`);
|
||||
} catch (e) {
|
||||
// wipe db
|
||||
console.error(e);
|
||||
}
|
||||
} else {
|
||||
throw new Error("OPFS not supported!");
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this.#db?.close();
|
||||
this.#db = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do database migration
|
||||
*/
|
||||
@ -56,34 +68,7 @@ export class WorkerRelay {
|
||||
event(ev: NostrEvent) {
|
||||
let eventInserted = false;
|
||||
this.#db?.transaction(db => {
|
||||
const legacyReplacable = [0, 3, 41];
|
||||
if (legacyReplacable.includes(ev.kind) || (ev.kind >= 10_000 && ev.kind < 20_000)) {
|
||||
db.exec("delete from events where kind = ? and pubkey = ?", {
|
||||
bind: [ev.kind, ev.pubkey],
|
||||
});
|
||||
this.#log(`Deleted old kind=${ev.kind}, author=${ev.pubkey} (rows=${db.changes()})`);
|
||||
}
|
||||
if (ev.kind >= 30_000 && ev.kind < 40_000) {
|
||||
const dTag = ev.tags.find(a => a[0] === "d")![1];
|
||||
db.exec(
|
||||
"delete from events where id in (select id from events, tags where events.id = tags.event_id and tags.key = ? and tags.value = ?)",
|
||||
{
|
||||
bind: ["d", dTag],
|
||||
},
|
||||
);
|
||||
this.#log(`Deleted old versions of: d=${dTag}, kind=${ev.kind}, author=${ev.pubkey} (rows=${db.changes()})`);
|
||||
}
|
||||
db.exec("insert or ignore into events(id, pubkey, created, kind, json) values(?,?,?,?,?)", {
|
||||
bind: [ev.id, ev.pubkey, ev.created_at, ev.kind, JSON.stringify(ev)],
|
||||
});
|
||||
eventInserted = (this.#db?.changes() as number) > 0;
|
||||
if (eventInserted) {
|
||||
for (const t of ev.tags.filter(a => a[0].length === 1)) {
|
||||
db.exec("insert into tags(event_id, key, value) values(?, ?, ?)", {
|
||||
bind: [ev.id, t[0], t[1]],
|
||||
});
|
||||
}
|
||||
}
|
||||
eventInserted = this.#insertEvent(db, ev);
|
||||
});
|
||||
if (eventInserted) {
|
||||
this.#log(`Inserted: kind=${ev.kind},authors=${ev.pubkey},id=${ev.id}`);
|
||||
@ -91,29 +76,125 @@ export class WorkerRelay {
|
||||
return eventInserted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write multiple events
|
||||
*/
|
||||
eventBatch(evs: Array<NostrEvent>) {
|
||||
let eventsInserted: Array<NostrEvent> = [];
|
||||
this.#db?.transaction(db => {
|
||||
for (const ev of evs) {
|
||||
if (this.#insertEvent(db, ev)) {
|
||||
eventsInserted.push(ev);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (eventsInserted.length > 0) {
|
||||
this.#log(`Inserted Batch: ${eventsInserted.length}/${evs.length}`);
|
||||
}
|
||||
return eventsInserted.length > 0;
|
||||
}
|
||||
|
||||
#insertEvent(db: Database, ev: NostrEvent) {
|
||||
const legacyReplacable = [0, 3, 41];
|
||||
if (legacyReplacable.includes(ev.kind) || (ev.kind >= 10_000 && ev.kind < 20_000)) {
|
||||
db.exec("delete from events where kind = ? and pubkey = ? and created < ?", {
|
||||
bind: [ev.kind, ev.pubkey, ev.created_at],
|
||||
});
|
||||
const oldDeleted = db.changes();
|
||||
if (oldDeleted === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (ev.kind >= 30_000 && ev.kind < 40_000) {
|
||||
const dTag = ev.tags.find(a => a[0] === "d")![1];
|
||||
db.exec(
|
||||
"delete from events where id in (select id from events, tags where events.id = tags.event_id and tags.key = ? and tags.value = ?)",
|
||||
{
|
||||
bind: ["d", dTag],
|
||||
},
|
||||
);
|
||||
const oldDeleted = db.changes();
|
||||
if (oldDeleted === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
db.exec("insert or ignore into events(id, pubkey, created, kind, json) values(?,?,?,?,?)", {
|
||||
bind: [ev.id, ev.pubkey, ev.created_at, ev.kind, JSON.stringify(ev)],
|
||||
});
|
||||
let eventInserted = (this.#db?.changes() as number) > 0;
|
||||
if (eventInserted) {
|
||||
for (const t of ev.tags.filter(a => a[0].length === 1)) {
|
||||
db.exec("insert into tags(event_id, key, value) values(?, ?, ?)", {
|
||||
bind: [ev.id, t[0], t[1]],
|
||||
});
|
||||
}
|
||||
}
|
||||
return eventInserted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query relay by nostr filter
|
||||
*/
|
||||
req(req: ReqFilter) {
|
||||
const start = unixNowMs();
|
||||
|
||||
const [sql, params] = this.#buildQuery(req);
|
||||
const rows = this.#db?.exec(sql, {
|
||||
bind: params,
|
||||
returnValue: "resultRows",
|
||||
});
|
||||
const results = rows?.map(a => JSON.parse(a[0] as string) as NostrEvent) ?? [];
|
||||
const time = unixNowMs() - start;
|
||||
this.#log(`Query results took ${time.toLocaleString()}ms`);
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count results by nostr filter
|
||||
*/
|
||||
count(req: ReqFilter) {
|
||||
const start = unixNowMs();
|
||||
const [sql, params] = this.#buildQuery(req, true);
|
||||
const rows = this.#db?.exec(sql, {
|
||||
bind: params,
|
||||
returnValue: "resultRows",
|
||||
});
|
||||
const results = (rows?.at(0)?.at(0) as number | undefined) ?? 0;
|
||||
const time = unixNowMs() - start;
|
||||
this.#log(`Query count results took ${time.toLocaleString()}ms`);
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a summary about events table
|
||||
*/
|
||||
summary() {
|
||||
const res = this.#db?.exec("select kind, count(*) from events group by kind order by 2 desc", {
|
||||
returnValue: "resultRows",
|
||||
});
|
||||
return Object.fromEntries(res?.map(a => [String(a[0]), a[1] as number]) ?? []);
|
||||
}
|
||||
|
||||
#buildQuery(req: ReqFilter, count = false) {
|
||||
const conditions: Array<string> = [];
|
||||
const params: Array<any> = [];
|
||||
|
||||
const repeatParams = (n: number) => {
|
||||
const ret = [];
|
||||
const ret: Array<string> = [];
|
||||
for (let x = 0; x < n; x++) {
|
||||
ret.push("?");
|
||||
}
|
||||
return ret.join(", ");
|
||||
};
|
||||
|
||||
let sql = `select json from events`;
|
||||
let sql = `select ${count ? "count(json)" : "json"} from events`;
|
||||
const tags = Object.entries(req).filter(([k]) => k.startsWith("#"));
|
||||
for (const [key, values] of tags) {
|
||||
const vArray = values as Array<string>;
|
||||
sql += ` inner join tags on events.id = tags.event_id and tags.key = ? and tags.value in (${repeatParams(
|
||||
vArray.length,
|
||||
)})`;
|
||||
params.push(key);
|
||||
params.push(key.slice(1));
|
||||
params.push(...vArray);
|
||||
}
|
||||
if (req.ids) {
|
||||
@ -142,13 +223,7 @@ export class WorkerRelay {
|
||||
if (req.limit) {
|
||||
sql += ` order by created desc limit ${req.limit}`;
|
||||
}
|
||||
|
||||
this.#log(`Made query ${sql} from ${JSON.stringify(req)}`);
|
||||
const rows = this.#db?.exec(sql, {
|
||||
bind: params,
|
||||
returnValue: "resultRows",
|
||||
});
|
||||
return rows?.map(a => JSON.parse(a[0] as string) as NostrEvent) ?? [];
|
||||
return [sql, params];
|
||||
}
|
||||
|
||||
#migrate_v1() {
|
||||
|
@ -1,6 +1,6 @@
|
||||
export interface WorkerMessage<T> {
|
||||
id: string;
|
||||
cmd: "reply" | "init" | "open" | "migrate" | "event" | "req";
|
||||
cmd: "reply" | "init" | "open" | "migrate" | "event" | "req" | "count" | "summary";
|
||||
args: T;
|
||||
}
|
||||
|
||||
@ -27,3 +27,7 @@ export interface ReqFilter {
|
||||
not?: ReqFilter;
|
||||
[key: string]: Array<string> | Array<number> | string | number | undefined | ReqFilter;
|
||||
}
|
||||
|
||||
export function unixNowMs() {
|
||||
return new Date().getTime();
|
||||
}
|
@ -13,43 +13,77 @@ async function reply<T>(id: string, obj?: T) {
|
||||
} as WorkerMessage<T>);
|
||||
}
|
||||
|
||||
// Event inserter queue
|
||||
let eventWriteQueue: Array<NostrEvent> = [];
|
||||
async function insertBatch() {
|
||||
if (eventWriteQueue.length > 0) {
|
||||
relay.eventBatch(eventWriteQueue);
|
||||
eventWriteQueue = [];
|
||||
}
|
||||
setTimeout(() => insertBatch(), 100);
|
||||
}
|
||||
setTimeout(() => insertBatch(), 100);
|
||||
|
||||
globalThis.onclose = () => {
|
||||
relay.close();
|
||||
};
|
||||
|
||||
globalThis.onmessage = async ev => {
|
||||
//console.debug(ev);
|
||||
|
||||
const msg = ev.data as WorkerMessage<any>;
|
||||
switch (msg.cmd) {
|
||||
case "init": {
|
||||
await relay.init();
|
||||
reply(msg.id, true);
|
||||
break;
|
||||
}
|
||||
case "open": {
|
||||
await relay.open("/relay.db");
|
||||
reply(msg.id, true);
|
||||
break;
|
||||
}
|
||||
case "migrate": {
|
||||
await relay.migrate();
|
||||
reply(msg.id, true);
|
||||
break;
|
||||
}
|
||||
case "event": {
|
||||
await relay.event(msg.args as NostrEvent);
|
||||
reply(msg.id, true);
|
||||
break;
|
||||
}
|
||||
case "req": {
|
||||
const req = msg.args as ReqCommand;
|
||||
const results = [];
|
||||
for (const r of req.slice(2)) {
|
||||
results.push(...(await relay.req(r as ReqFilter)));
|
||||
try {
|
||||
switch (msg.cmd) {
|
||||
case "init": {
|
||||
await relay.init();
|
||||
reply(msg.id, true);
|
||||
break;
|
||||
}
|
||||
case "open": {
|
||||
await relay.open("/relay.db");
|
||||
reply(msg.id, true);
|
||||
break;
|
||||
}
|
||||
case "migrate": {
|
||||
relay.migrate();
|
||||
reply(msg.id, true);
|
||||
break;
|
||||
}
|
||||
case "event": {
|
||||
eventWriteQueue.push(msg.args as NostrEvent);
|
||||
reply(msg.id, true);
|
||||
break;
|
||||
}
|
||||
case "req": {
|
||||
const req = msg.args as ReqCommand;
|
||||
const results = [];
|
||||
for (const r of req.slice(2)) {
|
||||
results.push(...relay.req(r as ReqFilter));
|
||||
}
|
||||
reply(msg.id, results);
|
||||
break;
|
||||
}
|
||||
case "count": {
|
||||
const req = msg.args as ReqCommand;
|
||||
let results = 0;
|
||||
for (const r of req.slice(2)) {
|
||||
const c = relay.count(r as ReqFilter);
|
||||
results += c;
|
||||
}
|
||||
reply(msg.id, results);
|
||||
break;
|
||||
}
|
||||
case "summary": {
|
||||
const res = relay.summary();
|
||||
reply(msg.id, res);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
reply(msg.id, { error: "Unknown command" });
|
||||
break;
|
||||
}
|
||||
reply(msg.id, results);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
reply(msg.id, { error: "Unknown command" });
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
reply(msg.id, { error: e });
|
||||
}
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user