use @snort/system cache

This commit is contained in:
2023-06-15 12:03:05 +01:00
parent c2a3a706de
commit fc11381ccd
79 changed files with 679 additions and 524 deletions

148
packages/system/src/cache/UserCache.ts vendored Normal file
View File

@ -0,0 +1,148 @@
import { db, MetadataCache } from ".";
import { fetchNip05Pubkey, FeedCache, LNURL } from "@snort/shared";
export class UserProfileCache extends FeedCache<MetadataCache> {
#zapperQueue: Array<{ pubkey: string; lnurl: string }> = [];
#nip5Queue: Array<{ pubkey: string; nip05: string }> = [];
constructor() {
super("UserCache", db.users);
this.#processZapperQueue();
this.#processNip5Queue();
}
key(of: MetadataCache): string {
return of.pubkey;
}
override async preload(follows?: Array<string>): Promise<void> {
await super.preload();
// load follows profiles
if (follows) {
await this.buffer(follows);
}
}
async search(q: string): Promise<Array<MetadataCache>> {
if (db.ready) {
// on-disk cache will always have more data
return (
await db.users
.where("npub")
.startsWithIgnoreCase(q)
.or("name")
.startsWithIgnoreCase(q)
.or("display_name")
.startsWithIgnoreCase(q)
.or("nip05")
.startsWithIgnoreCase(q)
.toArray()
).slice(0, 5);
} else {
return [...this.cache.values()]
.filter(user => {
const profile = user as MetadataCache;
return (
profile.name?.includes(q) ||
profile.npub?.includes(q) ||
profile.display_name?.includes(q) ||
profile.nip05?.includes(q)
);
})
.slice(0, 5);
}
}
/**
* Try to update the profile metadata cache with a new version
* @param m Profile metadata
* @returns
*/
override async update(m: MetadataCache) {
const updateType = await super.update(m);
if (updateType !== "refresh") {
const lnurl = m.lud16 ?? m.lud06;
if (lnurl) {
this.#zapperQueue.push({
pubkey: m.pubkey,
lnurl,
});
}
if (m.nip05) {
this.#nip5Queue.push({
pubkey: m.pubkey,
nip05: m.nip05,
});
}
}
return updateType;
}
takeSnapshot(): MetadataCache[] {
return [];
}
async #processZapperQueue() {
await this.#batchQueue(
this.#zapperQueue,
async i => {
const svc = new LNURL(i.lnurl);
await svc.load();
const p = this.getFromCache(i.pubkey);
if (p) {
await this.set({
...p,
zapService: svc.zapperPubkey,
});
}
},
5
);
setTimeout(() => this.#processZapperQueue(), 1_000);
}
async #processNip5Queue() {
await this.#batchQueue(
this.#nip5Queue,
async i => {
const [name, domain] = i.nip05.split("@");
const nip5pk = await fetchNip05Pubkey(name, domain);
const p = this.getFromCache(i.pubkey);
if (p) {
await this.set({
...p,
isNostrAddressValid: i.pubkey === nip5pk,
});
}
},
5
);
setTimeout(() => this.#processNip5Queue(), 1_000);
}
async #batchQueue<T>(queue: Array<T>, proc: (v: T) => Promise<void>, batchSize = 3) {
const batch = [];
while (queue.length > 0) {
const i = queue.shift();
if (i) {
batch.push(
(async () => {
try {
await proc(i);
} catch {
console.warn("Failed to process item", i);
}
batch.pop(); // pop any
})()
);
if (batch.length === batchSize) {
await Promise.all(batch);
}
} else {
await Promise.all(batch);
}
}
}
}

View File

@ -0,0 +1,29 @@
import { db, UsersRelays } from ".";
import { FeedCache } from "@snort/shared";
export class UserRelaysCache extends FeedCache<UsersRelays> {
constructor() {
super("UserRelays", db.userRelays);
}
key(of: UsersRelays): string {
return of.pubkey;
}
override async preload(follows?: Array<string>): Promise<void> {
await super.preload();
if (follows) {
await this.buffer(follows);
}
}
newest(): number {
let ret = 0;
this.cache.forEach(v => (ret = v.created_at > ret ? v.created_at : ret));
return ret;
}
takeSnapshot(): Array<UsersRelays> {
return [...this.cache.values()];
}
}

42
packages/system/src/cache/db.ts vendored Normal file
View File

@ -0,0 +1,42 @@
import { MetadataCache, RelayMetrics, UsersRelays } from ".";
import { NostrEvent } from "../Nostr";
import Dexie, { Table } from "dexie";
const NAME = "snort-system";
const VERSION = 1;
const STORES = {
users: "++pubkey, name, display_name, picture, nip05, npub",
relays: "++addr",
userRelays: "++pubkey",
events: "++id, pubkey, created_at"
};
export class SnortSystemDb extends Dexie {
ready = false;
users!: Table<MetadataCache>;
relayMetrics!: Table<RelayMetrics>;
userRelays!: Table<UsersRelays>;
events!: Table<NostrEvent>;
dms!: Table<NostrEvent>;
constructor() {
super(NAME);
this.version(VERSION).stores(STORES);
}
isAvailable() {
if ("indexedDB" in window) {
return new Promise<boolean>(resolve => {
const req = window.indexedDB.open("dummy", 1);
req.onsuccess = () => {
resolve(true);
};
req.onerror = () => {
resolve(false);
};
});
}
return Promise.resolve(false);
}
}

View File

@ -1,5 +1,8 @@
import { HexKey, NostrEvent, UserMetadata } from "..";
import { hexToBech32, unixNowMs } from "../Utils";
import { FullRelaySettings, HexKey, NostrEvent, UserMetadata } from "..";
import { hexToBech32, unixNowMs } from "@snort/shared";
import { SnortSystemDb } from "./db";
export const db = new SnortSystemDb();
export interface MetadataCache extends UserMetadata {
/**
@ -33,6 +36,19 @@ export interface MetadataCache extends UserMetadata {
isNostrAddressValid: boolean;
}
export interface RelayMetrics {
addr: string;
events: number;
disconnects: number;
latency: number[];
}
export interface UsersRelays {
pubkey: string;
created_at: number;
relays: FullRelaySettings[];
}
export function mapEventToProfile(ev: NostrEvent) {
try {
const data: UserMetadata = JSON.parse(ev.content);
@ -54,23 +70,4 @@ export function mapEventToProfile(ev: NostrEvent) {
} catch (e) {
console.error("Failed to parse JSON", ev, e);
}
}
export interface CacheStore<T> {
preload(): Promise<void>;
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>): Promise<void>;
update<TCachedWithCreated extends T & { created: number; loaded: number }>(m: TCachedWithCreated): Promise<"new" | "updated" | "refresh" | "no_change">
/**
* Loads a list of rows from disk cache
* @param keys List of ids to load
* @returns Keys that do not exist on disk cache
*/
buffer(keys: Array<string>): Promise<Array<string>>;
clear(): Promise<void>;
}
}