feat: automate social graph
This commit is contained in:
117
packages/app/src/Cache/UserFollowsWorker.ts
Normal file
117
packages/app/src/Cache/UserFollowsWorker.ts
Normal file
@ -0,0 +1,117 @@
|
||||
import { CachedTable, CacheEvents, removeUndefined, unixNowMs, unwrap } from "@snort/shared";
|
||||
import { EventKind, NostrEvent, UsersFollows } from "@snort/system";
|
||||
import { WorkerRelayInterface } from "@snort/worker-relay";
|
||||
import debug from "debug";
|
||||
import EventEmitter from "eventemitter3";
|
||||
|
||||
export class UserFollowsWorker extends EventEmitter<CacheEvents> implements CachedTable<UsersFollows> {
|
||||
#relay: WorkerRelayInterface;
|
||||
#keys = new Set<string>();
|
||||
#cache = new Map<string, UsersFollows>();
|
||||
#log = debug("UserFollowsWorker");
|
||||
|
||||
constructor(relay: WorkerRelayInterface) {
|
||||
super();
|
||||
this.#relay = relay;
|
||||
}
|
||||
|
||||
async preload() {
|
||||
const start = unixNowMs();
|
||||
const profiles = await this.#relay.query([
|
||||
"REQ",
|
||||
"profiles-preload",
|
||||
{
|
||||
kinds: [3],
|
||||
},
|
||||
]);
|
||||
this.#cache = new Map<string, UsersFollows>(profiles.map(a => [a.pubkey, unwrap(mapEventToUserFollows(a))]));
|
||||
this.#keys = new Set<string>(this.#cache.keys());
|
||||
this.#log(`Loaded %d/%d in %d ms`, this.#cache.size, this.#keys.size, (unixNowMs() - start).toLocaleString());
|
||||
}
|
||||
|
||||
keysOnTable(): string[] {
|
||||
return [...this.#keys];
|
||||
}
|
||||
|
||||
getFromCache(key?: string | undefined): UsersFollows | undefined {
|
||||
if (key) {
|
||||
return this.#cache.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
discover(ev: NostrEvent) {
|
||||
this.#keys.add(ev.pubkey);
|
||||
}
|
||||
|
||||
async get(key?: string | undefined): Promise<UsersFollows | undefined> {
|
||||
if (key) {
|
||||
const res = await this.bulkGet([key]);
|
||||
if (res.length > 0) {
|
||||
return res[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async bulkGet(keys: string[]) {
|
||||
if (keys.length === 0) return [];
|
||||
|
||||
const results = await this.#relay.query([
|
||||
"REQ",
|
||||
"UserFollowsWorker.bulkGet",
|
||||
{
|
||||
authors: keys,
|
||||
kinds: [3],
|
||||
},
|
||||
]);
|
||||
const mapped = removeUndefined(results.map(a => mapEventToUserFollows(a)));
|
||||
for (const pf of mapped) {
|
||||
this.#cache.set(this.key(pf), pf);
|
||||
}
|
||||
this.emit(
|
||||
"change",
|
||||
mapped.map(a => this.key(a)),
|
||||
);
|
||||
return mapped;
|
||||
}
|
||||
|
||||
async set(obj: UsersFollows) {
|
||||
this.#keys.add(this.key(obj));
|
||||
}
|
||||
|
||||
async bulkSet(obj: UsersFollows[] | readonly UsersFollows[]) {
|
||||
const mapped = obj.map(a => this.key(a));
|
||||
mapped.forEach(a => this.#keys.add(a));
|
||||
// todo: store in cache
|
||||
this.emit("change", mapped);
|
||||
}
|
||||
|
||||
async update(): Promise<"new" | "refresh" | "updated" | "no_change"> {
|
||||
// do nothing
|
||||
return "refresh";
|
||||
}
|
||||
|
||||
async buffer(keys: string[]): Promise<string[]> {
|
||||
const missing = keys.filter(a => !this.#keys.has(a));
|
||||
const res = await this.bulkGet(missing);
|
||||
return missing.filter(a => !res.some(b => this.key(b) === a));
|
||||
}
|
||||
|
||||
key(of: UsersFollows): string {
|
||||
return of.pubkey;
|
||||
}
|
||||
|
||||
snapshot(): UsersFollows[] {
|
||||
return [...this.#cache.values()];
|
||||
}
|
||||
}
|
||||
|
||||
export function mapEventToUserFollows(ev: NostrEvent): UsersFollows | undefined {
|
||||
if (ev.kind !== EventKind.ContactList) return;
|
||||
|
||||
return {
|
||||
pubkey: ev.pubkey,
|
||||
loaded: unixNowMs(),
|
||||
created: ev.created_at,
|
||||
follows: ev.tags,
|
||||
};
|
||||
}
|
@ -6,6 +6,7 @@ import WorkerRelayPath from "@snort/worker-relay/dist/worker?worker&url";
|
||||
import { EventCacheWorker } from "./EventCacheWorker";
|
||||
import { GiftWrapCache } from "./GiftWrapCache";
|
||||
import { ProfileCacheRelayWorker } from "./ProfileWorkerCache";
|
||||
import { UserFollowsWorker } from "./UserFollowsWorker";
|
||||
|
||||
export const Relay = new WorkerRelayInterface(WorkerRelayPath);
|
||||
export async function initRelayWorker() {
|
||||
@ -20,6 +21,7 @@ export const SystemDb = new SnortSystemDb();
|
||||
export const UserRelays = new UserRelaysCache(SystemDb.userRelays);
|
||||
export const RelayMetrics = new RelayMetricCache(SystemDb.relayMetrics);
|
||||
|
||||
export const UserFollows = new UserFollowsWorker(Relay);
|
||||
export const UserCache = new ProfileCacheRelayWorker(Relay);
|
||||
export const EventsCache = new EventCacheWorker(Relay);
|
||||
|
||||
@ -32,6 +34,7 @@ export async function preload(follows?: Array<string>) {
|
||||
GiftsCache.preload(),
|
||||
UserRelays.preload(follows),
|
||||
EventsCache.preload(),
|
||||
UserFollows.preload(),
|
||||
];
|
||||
await Promise.all(preloads);
|
||||
}
|
||||
|
@ -2,13 +2,12 @@ import "./index.css";
|
||||
import "@szhsin/react-menu/dist/index.css";
|
||||
import "@/assets/fonts/inter.css";
|
||||
|
||||
import { socialGraphInstance } from "@snort/system";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
import { StrictMode } from "react";
|
||||
import * as ReactDOM from "react-dom/client";
|
||||
import { createBrowserRouter, RouteObject, RouterProvider } from "react-router-dom";
|
||||
|
||||
import { initRelayWorker, preload, Relay, UserCache } from "@/Cache";
|
||||
import { initRelayWorker, preload, UserCache } from "@/Cache";
|
||||
import { ThreadRoute } from "@/Components/Event/Thread";
|
||||
import { IntlProvider } from "@/Components/IntlProvider/IntlProvider";
|
||||
import { db } from "@/Db";
|
||||
@ -55,25 +54,10 @@ async function initSite() {
|
||||
updateRelayConnections(System, login.relays.item).catch(console.error);
|
||||
setupWebLNWalletConfig(Wallets);
|
||||
|
||||
Relay.query([
|
||||
"REQ",
|
||||
"preload-social-graph",
|
||||
{
|
||||
kinds: [3],
|
||||
},
|
||||
]).then(res => {
|
||||
for (const ev of res) {
|
||||
try {
|
||||
socialGraphInstance.handleEvent(ev);
|
||||
} catch (e) {
|
||||
console.error("Failed to handle contact list event from sql db", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
db.ready = await db.isAvailable();
|
||||
if (db.ready) {
|
||||
await preload(login.follows.item);
|
||||
await System.PreloadSocialGraph();
|
||||
}
|
||||
|
||||
queueMicrotask(() => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { removeUndefined, throwIfOffline } from "@snort/shared";
|
||||
import { mapEventToProfile, NostrEvent, NostrSystem, socialGraphInstance } from "@snort/system";
|
||||
import { mapEventToProfile, NostrEvent, NostrSystem } from "@snort/system";
|
||||
|
||||
import { EventsCache, Relay, RelayMetrics, SystemDb, UserCache, UserRelays } from "@/Cache";
|
||||
import { EventsCache, Relay, RelayMetrics, SystemDb, UserCache, UserFollows, UserRelays } from "@/Cache";
|
||||
import { addEventToFuzzySearch } from "@/Db/FuzzySearch";
|
||||
import { LoginStore } from "@/Utils/Login";
|
||||
import { hasWasm, WasmOptimizer } from "@/Utils/wasm";
|
||||
@ -10,13 +10,15 @@ import { hasWasm, WasmOptimizer } from "@/Utils/wasm";
|
||||
* Singleton nostr system
|
||||
*/
|
||||
export const System = new NostrSystem({
|
||||
relayCache: UserRelays,
|
||||
eventsCache: EventsCache,
|
||||
profileCache: UserCache,
|
||||
relays: UserRelays,
|
||||
events: EventsCache,
|
||||
profiles: UserCache,
|
||||
relayMetrics: RelayMetrics,
|
||||
cacheRelay: Relay,
|
||||
cachingRelay: Relay,
|
||||
contactLists: UserFollows,
|
||||
optimizer: hasWasm ? WasmOptimizer : undefined,
|
||||
db: SystemDb,
|
||||
buildFollowGraph: true,
|
||||
});
|
||||
|
||||
System.on("auth", async (c, r, cb) => {
|
||||
@ -31,7 +33,6 @@ System.on("event", (_, ev) => {
|
||||
Relay.event(ev);
|
||||
EventsCache.discover(ev);
|
||||
UserCache.discover(ev);
|
||||
socialGraphInstance.handleEvent(ev);
|
||||
addEventToFuzzySearch(ev);
|
||||
});
|
||||
|
||||
|
Reference in New Issue
Block a user