refactor: migrate chats to relay worker cache
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
Kieran 2024-01-23 22:16:43 +00:00
parent c968fa43a6
commit e9d9bf34d8
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
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[]) {
const mapped = obj.map(a => this.key(a));
mapped.forEach(a => this.#keys.add(a));
// todo: store in cache
this.emit("change", mapped);
}

View File

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

View File

@ -3,7 +3,6 @@ import { useRequestBuilder } from "@snort/system-react";
import { usePrevious } from "@uidotdev/usehooks";
import { useEffect, useMemo } from "react";
import { Nip4Chats, Nip28Chats } from "@/chat";
import { Nip28ChatSystem } from "@/chat/nip28";
import useEventPublisher from "@/Hooks/useEventPublisher";
import useLogin from "@/Hooks/useLogin";
@ -77,14 +76,6 @@ export default function useLoginFeed() {
.limit(10);
}
const n4Sub = Nip4Chats.subscription(login);
if (n4Sub) {
b.add(n4Sub);
}
const n28Sub = Nip28Chats.subscription(login);
if (n28Sub) {
b.add(n28Sub);
}
return b;
}, [login]);
@ -105,9 +96,6 @@ export default function useLoginFeed() {
setRelays(login, Object.fromEntries(parsedRelays), relays.created_at * 1000);
}
Nip4Chats.onEvent(loginFeed);
Nip28Chats.onEvent(loginFeed);
if (publisher) {
const subs = loginFeed.filter(
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 { 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 NoteToSelf from "@/Components/User/NoteToSelf";
import ProfileImage from "@/Components/User/ProfileImage";
@ -23,7 +23,7 @@ export default function MessagesPage() {
const { id } = useParams();
const pageWidth = usePageWidth();
const chats = useChatSystem();
const chats = useChatSystems();
const unreadCount = useMemo(() => chats.reduce((p, c) => p + c.unread, 0), [chats]);
@ -83,7 +83,7 @@ export default function MessagesPage() {
return (
<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="flex items-center justify-between p-2">
<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 { FormattedMessage, FormattedNumber } from "react-intl";
import { Chats, GiftsCache, Relay, RelayMetrics } from "@/Cache";
import { GiftsCache, Relay, RelayMetrics } from "@/Cache";
import AsyncButton from "@/Components/Button/AsyncButton";
export function CacheSettings() {
@ -12,7 +12,6 @@ export function CacheSettings() {
<FormattedMessage defaultMessage="Cache" id="DBiVK1" />
</h3>
<RelayCacheStats />
<CacheDetails cache={Chats} name={<FormattedMessage defaultMessage="Chats" id="ABAQyo" />} />
<CacheDetails cache={RelayMetrics} name={<FormattedMessage defaultMessage="Relay Metrics" id="tjpYlr" />} />
<CacheDetails cache={GiftsCache} name={<FormattedMessage defaultMessage="Gift Wraps" id="fjAcWo" />} />
</div>

View File

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

View File

@ -12,19 +12,18 @@ import {
TLVEntryType,
UserMetadata,
} 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 useLogin from "@/Hooks/useLogin";
import useModeration from "@/Hooks/useModeration";
import { findTag } from "@/Utils";
import { LoginSession } from "@/Utils/Login";
import { Nip4ChatSystem } from "./nip4";
import { Nip4Chats, Nip4ChatSystem } from "./nip4";
import { Nip24ChatSystem } from "./nip24";
import { Nip28ChatSystem } from "./nip28";
import { Nip29ChatSystem } from "./nip29";
import { Nip28Chats, Nip28ChatSystem } from "./nip28";
export enum ChatType {
DirectMessage = 1,
@ -66,16 +65,13 @@ export interface ChatSystem {
* Create a request for this system to get updates
*/
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
*/
@ -169,46 +165,30 @@ export function createEmptyChatObject(id: string, messages?: Array<TaggedNostrEv
throw new Error("Cant create new empty chat, unknown id");
}
export function useNip4Chat() {
const { publicKey } = useLogin(s => ({ publicKey: s.publicKey }));
return useSyncExternalStore(
c => Nip4Chats.hook(c),
() => Nip4Chats.snapshot(publicKey),
);
}
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();
export function useChatSystem(chat: ChatSystem) {
const login = useLogin();
const sub = useMemo(() => {
return chat.subscription(login);
}, [login.publicKey]);
const data = useRequestBuilder(sub);
const { isBlocked } = useModeration();
return [...nip4, ...nip28].filter(a => {
const authors = a.participants.filter(a => a.type === "pubkey").map(a => a.id);
return authors.length === 0 || !authors.every(a => isBlocked(a));
});
return useMemo(() => {
if (login.publicKey) {
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) {
@ -221,13 +201,7 @@ export function useChat(id: string) {
}
throw new Error("Unsupported chat system");
};
const store = getStore();
const ret = useSyncExternalStore(
c => store.hook(c),
() => {
return store.snapshot().find(a => a.id === id);
},
);
const ret = useChatSystem(getStore()).find(a => a.id === id);
const emptyChat = useEmptyChatSystem(ret === undefined ? id : undefined);
return ret ?? emptyChat;
}

View File

@ -1,4 +1,4 @@
import { ExternalStore, FeedCache, unwrap } from "@snort/shared";
import { unwrap } from "@snort/shared";
import {
decodeTLV,
encodeTLVEntries,
@ -11,15 +11,12 @@ import {
TLVEntryType,
UserMetadata,
} from "@snort/system";
import debug from "debug";
import { Chat, ChatParticipant, ChatSystem, ChatType, lastReadInChat } from "@/chat";
import { findTag } from "@/Utils";
import { LoginSession } from "@/Utils/Login";
export class Nip28ChatSystem extends ExternalStore<Array<Chat>> implements ChatSystem {
#cache: FeedCache<NostrEvent>;
#log = debug("NIP-28");
export class Nip28ChatSystem implements ChatSystem {
readonly ChannelKinds = [
EventKind.PublicChatChannel,
EventKind.PublicChatMessage,
@ -28,44 +25,26 @@ export class Nip28ChatSystem extends ExternalStore<Array<Chat>> implements ChatS
EventKind.PublicChatMuteUser,
];
constructor(cache: FeedCache<NostrEvent>) {
super();
this.#cache = cache;
}
subscription(session: LoginSession): RequestBuilder | undefined {
const chats = (session.extraChats ?? []).filter(a => a.startsWith("chat281"));
if (chats.length === 0) return;
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}`);
rb.withFilter()
.ids(chats.map(v => chatId(v)))
.kinds([EventKind.PublicChatChannel, EventKind.PublicChatMetadata]);
for (const c of chats) {
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])
.since(lastMessage === 0 ? undefined : lastMessage)
.kinds(this.ChannelKinds);
rb.withFilter().tag("e", [id]).kinds(this.ChannelKinds);
}
return rb;
}
async onEvent(evs: readonly TaggedNostrEvent[]) {
const dms = evs.filter(a => this.ChannelKinds.includes(a.kind));
if (dms.length > 0) {
await this.#cache.bulkSet(dms);
this.notifyChange();
}
}
listChats(): Chat[] {
const chats = this.#chatChannels();
listChats(pk: string, evs: Array<TaggedNostrEvent>): Chat[] {
const chats = this.#chatChannels(evs);
const ret = Object.entries(chats).map(([k, v]) => {
return Nip28ChatSystem.createChatObj(Nip28ChatSystem.chatId(k), v);
});
@ -121,10 +100,6 @@ export class Nip28ChatSystem extends ExternalStore<Array<Chat>> implements ChatS
} as Chat;
}
takeSnapshot(): Chat[] {
return this.listChats();
}
static #chatProfileFromMessages(messages: Array<NostrEvent>) {
const chatDefs = messages.filter(
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;
}
#chatChannels() {
const messages = this.#cache.snapshot();
const chats = messages.reduce(
#chatChannels(evs: Array<TaggedNostrEvent>) {
const chats = evs.reduce(
(acc, v) => {
const k = this.#chatId(v);
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 {
decodeTLV,
encodeTLVEntries,
@ -10,51 +9,23 @@ import {
TaggedNostrEvent,
TLVEntryType,
} from "@snort/system";
import { debug } from "debug";
import { Chat, ChatSystem, ChatType, inChatWith, lastReadInChat } from "@/chat";
import { LoginSession } from "@/Utils/Login";
export class Nip4ChatSystem extends ExternalStore<Array<Chat>> 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();
}
}
export class Nip4ChatSystem implements ChatSystem {
subscription(session: LoginSession) {
const pk = session.publicKey;
if (!pk || session.readonly) return;
const rb = new RequestBuilder(`nip4:${pk.slice(0, 12)}`);
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,
);
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);
rb.withFilter().authors([pk]).kinds([EventKind.DirectMessage]);
rb.withFilter().kinds([EventKind.DirectMessage]).tag("p", [pk]);
return rb;
}
takeSnapshot(p: string) {
return this.listChats(p);
}
listChats(pk: string): Chat[] {
const myDms = this.#nip4Events();
listChats(pk: string, evs: Array<TaggedNostrEvent>): Chat[] {
const myDms = this.#nip4Events(evs);
const chats = myDms.reduce(
(acc, v) => {
const chatId = inChatWith(v, pk);
@ -110,7 +81,9 @@ export class Nip4ChatSystem extends ExternalStore<Array<Chat>> implements ChatSy
} as Chat;
}
#nip4Events() {
return this.#cache.snapshot().filter(a => a.kind === EventKind.DirectMessage);
#nip4Events(evs: Array<TaggedNostrEvent>) {
return evs.filter(a => a.kind === EventKind.DirectMessage);
}
}
export const Nip4Chats = new Nip4ChatSystem();

View File

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

View File

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