refactor: migrate chats to relay worker cache

This commit is contained in:
2024-01-23 22:16:43 +00:00
parent c968fa43a6
commit e9d9bf34d8
11 changed files with 56 additions and 153 deletions

View File

@ -88,6 +88,7 @@ export class ProfileCacheRelayWorker extends EventEmitter<CacheEvents> implement
async bulkSet(obj: CachedMetadata[] | readonly CachedMetadata[]) { async bulkSet(obj: CachedMetadata[] | readonly CachedMetadata[]) {
const mapped = obj.map(a => this.key(a)); const mapped = obj.map(a => this.key(a));
mapped.forEach(a => this.#keys.add(a)); mapped.forEach(a => this.#keys.add(a));
// todo: store in cache
this.emit("change", mapped); this.emit("change", mapped);
} }

View File

@ -3,10 +3,9 @@ import { SnortSystemDb } from "@snort/system-web";
import { WorkerRelayInterface } from "@snort/worker-relay"; import { WorkerRelayInterface } from "@snort/worker-relay";
import WorkerRelayPath from "@snort/worker-relay/dist/worker?worker&url"; import WorkerRelayPath from "@snort/worker-relay/dist/worker?worker&url";
import { ChatCache } from "./ChatCache";
import { EventCacheWorker } from "./EventCacheWorker"; import { EventCacheWorker } from "./EventCacheWorker";
import { GiftWrapCache } from "./GiftWrapCache"; import { GiftWrapCache } from "./GiftWrapCache";
import { ProfileCacheRelayWorker } from "./ProfileWorkeCache"; import { ProfileCacheRelayWorker } from "./ProfileWorkerCache";
export const Relay = new WorkerRelayInterface(WorkerRelayPath); export const Relay = new WorkerRelayInterface(WorkerRelayPath);
export async function initRelayWorker() { export async function initRelayWorker() {
@ -24,13 +23,11 @@ export const RelayMetrics = new RelayMetricCache(SystemDb.relayMetrics);
export const UserCache = new ProfileCacheRelayWorker(Relay); export const UserCache = new ProfileCacheRelayWorker(Relay);
export const EventsCache = new EventCacheWorker(Relay); export const EventsCache = new EventCacheWorker(Relay);
export const Chats = new ChatCache();
export const GiftsCache = new GiftWrapCache(); export const GiftsCache = new GiftWrapCache();
export async function preload(follows?: Array<string>) { export async function preload(follows?: Array<string>) {
const preloads = [ const preloads = [
UserCache.preload(), UserCache.preload(),
Chats.preload(),
RelayMetrics.preload(), RelayMetrics.preload(),
GiftsCache.preload(), GiftsCache.preload(),
UserRelays.preload(follows), UserRelays.preload(follows),

View File

@ -3,7 +3,6 @@ import { useRequestBuilder } from "@snort/system-react";
import { usePrevious } from "@uidotdev/usehooks"; import { usePrevious } from "@uidotdev/usehooks";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { Nip4Chats, Nip28Chats } from "@/chat";
import { Nip28ChatSystem } from "@/chat/nip28"; import { Nip28ChatSystem } from "@/chat/nip28";
import useEventPublisher from "@/Hooks/useEventPublisher"; import useEventPublisher from "@/Hooks/useEventPublisher";
import useLogin from "@/Hooks/useLogin"; import useLogin from "@/Hooks/useLogin";
@ -77,14 +76,6 @@ export default function useLoginFeed() {
.limit(10); .limit(10);
} }
const n4Sub = Nip4Chats.subscription(login);
if (n4Sub) {
b.add(n4Sub);
}
const n28Sub = Nip28Chats.subscription(login);
if (n28Sub) {
b.add(n28Sub);
}
return b; return b;
}, [login]); }, [login]);
@ -105,9 +96,6 @@ export default function useLoginFeed() {
setRelays(login, Object.fromEntries(parsedRelays), relays.created_at * 1000); setRelays(login, Object.fromEntries(parsedRelays), relays.created_at * 1000);
} }
Nip4Chats.onEvent(loginFeed);
Nip28Chats.onEvent(loginFeed);
if (publisher) { if (publisher) {
const subs = loginFeed.filter( const subs = loginFeed.filter(
a => a.kind === EventKind.SnortSubscriptions && a.pubkey === bech32ToHex(SnortPubKey), a => a.kind === EventKind.SnortSubscriptions && a.pubkey === bech32ToHex(SnortPubKey),

View File

@ -3,7 +3,7 @@ import React, { useMemo } from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { Chat, ChatType, useChatSystem } from "@/chat"; import { Chat, ChatType, useChatSystems } from "@/chat";
import NoteTime from "@/Components/Event/Note/NoteTime"; import NoteTime from "@/Components/Event/Note/NoteTime";
import NoteToSelf from "@/Components/User/NoteToSelf"; import NoteToSelf from "@/Components/User/NoteToSelf";
import ProfileImage from "@/Components/User/ProfileImage"; import ProfileImage from "@/Components/User/ProfileImage";
@ -23,7 +23,7 @@ export default function MessagesPage() {
const { id } = useParams(); const { id } = useParams();
const pageWidth = usePageWidth(); const pageWidth = usePageWidth();
const chats = useChatSystem(); const chats = useChatSystems();
const unreadCount = useMemo(() => chats.reduce((p, c) => p + c.unread, 0), [chats]); const unreadCount = useMemo(() => chats.reduce((p, c) => p + c.unread, 0), [chats]);
@ -83,7 +83,7 @@ export default function MessagesPage() {
return ( return (
<div className="flex flex-1 md:h-screen md:overflow-hidden"> <div className="flex flex-1 md:h-screen md:overflow-hidden">
{pageWidth >= TwoCol && !id && ( {pageWidth >= TwoCol && (
<div className="overflow-y-auto md:h-screen p-1 w-full md:w-1/3 flex-shrink-0"> <div className="overflow-y-auto md:h-screen p-1 w-full md:w-1/3 flex-shrink-0">
<div className="flex items-center justify-between p-2"> <div className="flex items-center justify-between p-2">
<button disabled={unreadCount <= 0} type="button" className="text-sm font-semibold"> <button disabled={unreadCount <= 0} type="button" className="text-sm font-semibold">

View File

@ -2,7 +2,7 @@ import { FeedCache } from "@snort/shared";
import { ReactNode, useEffect, useState, useSyncExternalStore } from "react"; import { ReactNode, useEffect, useState, useSyncExternalStore } from "react";
import { FormattedMessage, FormattedNumber } from "react-intl"; import { FormattedMessage, FormattedNumber } from "react-intl";
import { Chats, GiftsCache, Relay, RelayMetrics } from "@/Cache"; import { GiftsCache, Relay, RelayMetrics } from "@/Cache";
import AsyncButton from "@/Components/Button/AsyncButton"; import AsyncButton from "@/Components/Button/AsyncButton";
export function CacheSettings() { export function CacheSettings() {
@ -12,7 +12,6 @@ export function CacheSettings() {
<FormattedMessage defaultMessage="Cache" id="DBiVK1" /> <FormattedMessage defaultMessage="Cache" id="DBiVK1" />
</h3> </h3>
<RelayCacheStats /> <RelayCacheStats />
<CacheDetails cache={Chats} name={<FormattedMessage defaultMessage="Chats" id="ABAQyo" />} />
<CacheDetails cache={RelayMetrics} name={<FormattedMessage defaultMessage="Relay Metrics" id="tjpYlr" />} /> <CacheDetails cache={RelayMetrics} name={<FormattedMessage defaultMessage="Relay Metrics" id="tjpYlr" />} />
<CacheDetails cache={GiftsCache} name={<FormattedMessage defaultMessage="Gift Wraps" id="fjAcWo" />} /> <CacheDetails cache={GiftsCache} name={<FormattedMessage defaultMessage="Gift Wraps" id="fjAcWo" />} />
</div> </div>

View File

@ -12,7 +12,7 @@ import {
UserMetadata, UserMetadata,
} from "@snort/system"; } from "@snort/system";
import { Chats, GiftsCache } from "@/Cache"; import { GiftsCache } from "@/Cache";
import SnortApi from "@/External/SnortApi"; import SnortApi from "@/External/SnortApi";
import { bech32ToHex, dedupeById, deleteRefCode, getCountry, sanitizeRelayUrl, unwrap } from "@/Utils"; import { bech32ToHex, dedupeById, deleteRefCode, getCountry, sanitizeRelayUrl, unwrap } from "@/Utils";
import { Blasters } from "@/Utils/Const"; import { Blasters } from "@/Utils/Const";
@ -68,7 +68,6 @@ export function updatePreferences(id: string, p: UserPreferences) {
export function logout(id: string) { export function logout(id: string) {
LoginStore.removeSession(id); LoginStore.removeSession(id);
GiftsCache.clear(); GiftsCache.clear();
Chats.clear();
deleteRefCode(); deleteRefCode();
localStorage.clear(); localStorage.clear();
} }

View File

@ -12,19 +12,18 @@ import {
TLVEntryType, TLVEntryType,
UserMetadata, UserMetadata,
} from "@snort/system"; } from "@snort/system";
import { useSyncExternalStore } from "react"; import { useRequestBuilder } from "@snort/system-react";
import { useMemo } from "react";
import { Chats, GiftsCache } from "@/Cache";
import { useEmptyChatSystem } from "@/Hooks/useEmptyChatSystem"; import { useEmptyChatSystem } from "@/Hooks/useEmptyChatSystem";
import useLogin from "@/Hooks/useLogin"; import useLogin from "@/Hooks/useLogin";
import useModeration from "@/Hooks/useModeration"; import useModeration from "@/Hooks/useModeration";
import { findTag } from "@/Utils"; import { findTag } from "@/Utils";
import { LoginSession } from "@/Utils/Login"; import { LoginSession } from "@/Utils/Login";
import { Nip4ChatSystem } from "./nip4"; import { Nip4Chats, Nip4ChatSystem } from "./nip4";
import { Nip24ChatSystem } from "./nip24"; import { Nip24ChatSystem } from "./nip24";
import { Nip28ChatSystem } from "./nip28"; import { Nip28Chats, Nip28ChatSystem } from "./nip28";
import { Nip29ChatSystem } from "./nip29";
export enum ChatType { export enum ChatType {
DirectMessage = 1, DirectMessage = 1,
@ -66,16 +65,13 @@ export interface ChatSystem {
* Create a request for this system to get updates * Create a request for this system to get updates
*/ */
subscription(session: LoginSession): RequestBuilder | undefined; subscription(session: LoginSession): RequestBuilder | undefined;
onEvent(evs: readonly TaggedNostrEvent[]): Promise<void> | void;
listChats(pk: string): Array<Chat>; /**
* Create a list of chats for a given pubkey and set of events
*/
listChats(pk: string, evs: Array<TaggedNostrEvent>): Array<Chat>;
} }
export const Nip4Chats = new Nip4ChatSystem(Chats);
export const Nip29Chats = new Nip29ChatSystem(Chats);
export const Nip24Chats = new Nip24ChatSystem(GiftsCache);
export const Nip28Chats = new Nip28ChatSystem(Chats);
/** /**
* Extract the P tag of the event * Extract the P tag of the event
*/ */
@ -169,46 +165,30 @@ export function createEmptyChatObject(id: string, messages?: Array<TaggedNostrEv
throw new Error("Cant create new empty chat, unknown id"); throw new Error("Cant create new empty chat, unknown id");
} }
export function useNip4Chat() { export function useChatSystem(chat: ChatSystem) {
const { publicKey } = useLogin(s => ({ publicKey: s.publicKey })); const login = useLogin();
return useSyncExternalStore( const sub = useMemo(() => {
c => Nip4Chats.hook(c), return chat.subscription(login);
() => Nip4Chats.snapshot(publicKey), }, [login.publicKey]);
); const data = useRequestBuilder(sub);
}
export function useNip29Chat() {
return useSyncExternalStore(
c => Nip29Chats.hook(c),
() => Nip29Chats.snapshot(),
);
}
export function useNip24Chat() {
const { publicKey } = useLogin(s => ({ publicKey: s.publicKey }));
return useSyncExternalStore(
c => Nip24Chats.hook(c),
() => Nip24Chats.snapshot(publicKey),
);
}
export function useNip28Chat() {
return useSyncExternalStore(
c => Nip28Chats.hook(c),
() => Nip28Chats.snapshot(),
);
}
export function useChatSystem() {
const nip4 = useNip4Chat();
//const nip24 = useNip24Chat();
const nip28 = useNip28Chat();
const { isBlocked } = useModeration(); const { isBlocked } = useModeration();
return [...nip4, ...nip28].filter(a => { return useMemo(() => {
const authors = a.participants.filter(a => a.type === "pubkey").map(a => a.id); if (login.publicKey) {
return authors.length === 0 || !authors.every(a => isBlocked(a)); return chat.listChats(
}); login.publicKey,
data.filter(a => !isBlocked(a.pubkey)),
);
}
return [];
}, [login.publicKey, data]);
}
export function useChatSystems() {
const nip4 = useChatSystem(Nip4Chats);
const nip28 = useChatSystem(Nip28Chats);
return [...nip4, ...nip28];
} }
export function useChat(id: string) { export function useChat(id: string) {
@ -221,13 +201,7 @@ export function useChat(id: string) {
} }
throw new Error("Unsupported chat system"); throw new Error("Unsupported chat system");
}; };
const store = getStore(); const ret = useChatSystem(getStore()).find(a => a.id === id);
const ret = useSyncExternalStore(
c => store.hook(c),
() => {
return store.snapshot().find(a => a.id === id);
},
);
const emptyChat = useEmptyChatSystem(ret === undefined ? id : undefined); const emptyChat = useEmptyChatSystem(ret === undefined ? id : undefined);
return ret ?? emptyChat; return ret ?? emptyChat;
} }

View File

@ -1,4 +1,4 @@
import { ExternalStore, FeedCache, unwrap } from "@snort/shared"; import { unwrap } from "@snort/shared";
import { import {
decodeTLV, decodeTLV,
encodeTLVEntries, encodeTLVEntries,
@ -11,15 +11,12 @@ import {
TLVEntryType, TLVEntryType,
UserMetadata, UserMetadata,
} from "@snort/system"; } from "@snort/system";
import debug from "debug";
import { Chat, ChatParticipant, ChatSystem, ChatType, lastReadInChat } from "@/chat"; import { Chat, ChatParticipant, ChatSystem, ChatType, lastReadInChat } from "@/chat";
import { findTag } from "@/Utils"; import { findTag } from "@/Utils";
import { LoginSession } from "@/Utils/Login"; import { LoginSession } from "@/Utils/Login";
export class Nip28ChatSystem extends ExternalStore<Array<Chat>> implements ChatSystem { export class Nip28ChatSystem implements ChatSystem {
#cache: FeedCache<NostrEvent>;
#log = debug("NIP-28");
readonly ChannelKinds = [ readonly ChannelKinds = [
EventKind.PublicChatChannel, EventKind.PublicChatChannel,
EventKind.PublicChatMessage, EventKind.PublicChatMessage,
@ -28,44 +25,26 @@ export class Nip28ChatSystem extends ExternalStore<Array<Chat>> implements ChatS
EventKind.PublicChatMuteUser, EventKind.PublicChatMuteUser,
]; ];
constructor(cache: FeedCache<NostrEvent>) {
super();
this.#cache = cache;
}
subscription(session: LoginSession): RequestBuilder | undefined { subscription(session: LoginSession): RequestBuilder | undefined {
const chats = (session.extraChats ?? []).filter(a => a.startsWith("chat281")); const chats = (session.extraChats ?? []).filter(a => a.startsWith("chat281"));
if (chats.length === 0) return; if (chats.length === 0) return;
const chatId = (v: string) => unwrap(decodeTLV(v).find(a => a.type === TLVEntryType.Special)).value as string; const chatId = (v: string) => unwrap(decodeTLV(v).find(a => a.type === TLVEntryType.Special)).value as string;
const messages = this.#chatChannels();
const rb = new RequestBuilder(`nip28:${session.id}`); const rb = new RequestBuilder(`nip28:${session.id}`);
rb.withFilter() rb.withFilter()
.ids(chats.map(v => chatId(v))) .ids(chats.map(v => chatId(v)))
.kinds([EventKind.PublicChatChannel, EventKind.PublicChatMetadata]); .kinds([EventKind.PublicChatChannel, EventKind.PublicChatMetadata]);
for (const c of chats) { for (const c of chats) {
const id = chatId(c); const id = chatId(c);
const lastMessage = messages[id]?.reduce((acc, v) => (v.created_at > acc ? v.created_at : acc), 0) ?? 0; rb.withFilter().tag("e", [id]).kinds(this.ChannelKinds);
rb.withFilter()
.tag("e", [id])
.since(lastMessage === 0 ? undefined : lastMessage)
.kinds(this.ChannelKinds);
} }
return rb; return rb;
} }
async onEvent(evs: readonly TaggedNostrEvent[]) { listChats(pk: string, evs: Array<TaggedNostrEvent>): Chat[] {
const dms = evs.filter(a => this.ChannelKinds.includes(a.kind)); const chats = this.#chatChannels(evs);
if (dms.length > 0) {
await this.#cache.bulkSet(dms);
this.notifyChange();
}
}
listChats(): Chat[] {
const chats = this.#chatChannels();
const ret = Object.entries(chats).map(([k, v]) => { const ret = Object.entries(chats).map(([k, v]) => {
return Nip28ChatSystem.createChatObj(Nip28ChatSystem.chatId(k), v); return Nip28ChatSystem.createChatObj(Nip28ChatSystem.chatId(k), v);
}); });
@ -121,10 +100,6 @@ export class Nip28ChatSystem extends ExternalStore<Array<Chat>> implements ChatS
} as Chat; } as Chat;
} }
takeSnapshot(): Chat[] {
return this.listChats();
}
static #chatProfileFromMessages(messages: Array<NostrEvent>) { static #chatProfileFromMessages(messages: Array<NostrEvent>) {
const chatDefs = messages.filter( const chatDefs = messages.filter(
a => a.kind === EventKind.PublicChatChannel || a.kind === EventKind.PublicChatMetadata, a => a.kind === EventKind.PublicChatChannel || a.kind === EventKind.PublicChatMetadata,
@ -136,9 +111,8 @@ export class Nip28ChatSystem extends ExternalStore<Array<Chat>> implements ChatS
return chatDef ? (JSON.parse(chatDef.content) as UserMetadata) : undefined; return chatDef ? (JSON.parse(chatDef.content) as UserMetadata) : undefined;
} }
#chatChannels() { #chatChannels(evs: Array<TaggedNostrEvent>) {
const messages = this.#cache.snapshot(); const chats = evs.reduce(
const chats = messages.reduce(
(acc, v) => { (acc, v) => {
const k = this.#chatId(v); const k = this.#chatId(v);
if (k) { if (k) {
@ -162,3 +136,5 @@ export class Nip28ChatSystem extends ExternalStore<Array<Chat>> implements ChatS
} }
} }
} }
export const Nip28Chats = new Nip28ChatSystem();

View File

@ -1,4 +1,3 @@
import { ExternalStore, FeedCache } from "@snort/shared";
import { import {
decodeTLV, decodeTLV,
encodeTLVEntries, encodeTLVEntries,
@ -10,51 +9,23 @@ import {
TaggedNostrEvent, TaggedNostrEvent,
TLVEntryType, TLVEntryType,
} from "@snort/system"; } from "@snort/system";
import { debug } from "debug";
import { Chat, ChatSystem, ChatType, inChatWith, lastReadInChat } from "@/chat"; import { Chat, ChatSystem, ChatType, inChatWith, lastReadInChat } from "@/chat";
import { LoginSession } from "@/Utils/Login"; import { LoginSession } from "@/Utils/Login";
export class Nip4ChatSystem extends ExternalStore<Array<Chat>> implements ChatSystem { export class Nip4ChatSystem implements ChatSystem {
#cache: FeedCache<NostrEvent>;
#log = debug("NIP-04");
constructor(cache: FeedCache<NostrEvent>) {
super();
this.#cache = cache;
}
async onEvent(evs: readonly TaggedNostrEvent[]) {
const dms = evs.filter(a => a.kind === EventKind.DirectMessage && a.tags.some(b => b[0] === "p"));
if (dms.length > 0) {
await this.#cache.bulkSet(dms);
this.notifyChange();
}
}
subscription(session: LoginSession) { subscription(session: LoginSession) {
const pk = session.publicKey; const pk = session.publicKey;
if (!pk || session.readonly) return; if (!pk || session.readonly) return;
const rb = new RequestBuilder(`nip4:${pk.slice(0, 12)}`); const rb = new RequestBuilder(`nip4:${pk.slice(0, 12)}`);
const dms = this.#cache.snapshot(); rb.withFilter().authors([pk]).kinds([EventKind.DirectMessage]);
const dmSince = dms.reduce( rb.withFilter().kinds([EventKind.DirectMessage]).tag("p", [pk]);
(acc, v) => (v.created_at > acc && v.kind === EventKind.DirectMessage ? (acc = v.created_at) : acc),
0,
);
this.#log("Loading DMS since %s", new Date(dmSince * 1000));
rb.withFilter().authors([pk]).kinds([EventKind.DirectMessage]).since(dmSince);
rb.withFilter().kinds([EventKind.DirectMessage]).tag("p", [pk]).since(dmSince);
return rb; return rb;
} }
takeSnapshot(p: string) { listChats(pk: string, evs: Array<TaggedNostrEvent>): Chat[] {
return this.listChats(p); const myDms = this.#nip4Events(evs);
}
listChats(pk: string): Chat[] {
const myDms = this.#nip4Events();
const chats = myDms.reduce( const chats = myDms.reduce(
(acc, v) => { (acc, v) => {
const chatId = inChatWith(v, pk); const chatId = inChatWith(v, pk);
@ -110,7 +81,9 @@ export class Nip4ChatSystem extends ExternalStore<Array<Chat>> implements ChatSy
} as Chat; } as Chat;
} }
#nip4Events() { #nip4Events(evs: Array<TaggedNostrEvent>) {
return this.#cache.snapshot().filter(a => a.kind === EventKind.DirectMessage); return evs.filter(a => a.kind === EventKind.DirectMessage);
} }
} }
export const Nip4Chats = new Nip4ChatSystem();

View File

@ -330,9 +330,6 @@
"9wO4wJ": { "9wO4wJ": {
"defaultMessage": "Lightning Invoice" "defaultMessage": "Lightning Invoice"
}, },
"ABAQyo": {
"defaultMessage": "Chats"
},
"ADmfQT": { "ADmfQT": {
"defaultMessage": "Parent", "defaultMessage": "Parent",
"description": "Link to parent note in thread" "description": "Link to parent note in thread"

View File

@ -109,7 +109,6 @@
"9kSari": "Retry publishing", "9kSari": "Retry publishing",
"9pMqYs": "Nostr Address", "9pMqYs": "Nostr Address",
"9wO4wJ": "Lightning Invoice", "9wO4wJ": "Lightning Invoice",
"ABAQyo": "Chats",
"ADmfQT": "Parent", "ADmfQT": "Parent",
"ALdW69": "Note by {name}", "ALdW69": "Note by {name}",
"AN0Z7Q": "Muted Words", "AN0Z7Q": "Muted Words",