snort/packages/app/src/System/ProfileCache.ts

125 lines
4.0 KiB
TypeScript
Raw Normal View History

2023-06-01 08:54:25 +00:00
import { EventKind, HexKey, SystemInterface, TaggedRawEvent } from "System";
2023-03-28 14:34:01 +00:00
import { ProfileCacheExpire } from "Const";
2023-03-29 12:10:22 +00:00
import { mapEventToProfile, MetadataCache } from "Cache";
import { UserCache } from "Cache/UserCache";
2023-05-30 13:48:38 +00:00
import { PubkeyReplaceableNoteStore, RequestBuilder } from "System";
2023-05-24 10:12:23 +00:00
import { unixNowMs } from "SnortUtils";
import debug from "debug";
2023-03-28 14:34:01 +00:00
2023-05-30 13:48:38 +00:00
export class ProfileLoaderService {
2023-06-01 08:54:25 +00:00
#system: SystemInterface;
2023-05-30 13:48:38 +00:00
2023-03-28 14:34:01 +00:00
/**
* List of pubkeys to fetch metadata for
*/
WantsMetadata: Set<HexKey> = new Set();
readonly #log = debug("ProfileCache");
2023-06-01 08:54:25 +00:00
constructor(system: SystemInterface) {
2023-05-30 13:48:38 +00:00
this.#system = system;
2023-03-28 14:34:01 +00:00
this.#FetchMetadata();
}
/**
* Request profile metadata for a set of pubkeys
*/
TrackMetadata(pk: HexKey | Array<HexKey>) {
2023-05-10 13:24:22 +00:00
const bufferNow = [];
2023-03-28 14:34:01 +00:00
for (const p of Array.isArray(pk) ? pk : [pk]) {
2023-05-10 13:24:22 +00:00
if (p.length > 0 && this.WantsMetadata.add(p)) {
bufferNow.push(p);
2023-03-28 14:34:01 +00:00
}
}
2023-05-10 13:24:22 +00:00
UserCache.buffer(bufferNow);
2023-03-28 14:34:01 +00:00
}
/**
* Stop tracking metadata for a set of pubkeys
*/
UntrackMetadata(pk: HexKey | Array<HexKey>) {
for (const p of Array.isArray(pk) ? pk : [pk]) {
if (p.length > 0) {
this.WantsMetadata.delete(p);
}
}
}
async onProfileEvent(e: Readonly<TaggedRawEvent>) {
const profile = mapEventToProfile(e);
if (profile) {
await UserCache.update(profile);
2023-04-04 18:44:45 +00:00
}
}
2023-03-28 14:34:01 +00:00
async #FetchMetadata() {
const missingFromCache = await UserCache.buffer([...this.WantsMetadata]);
const expire = unixNowMs() - ProfileCacheExpire;
const expired = [...this.WantsMetadata]
.filter(a => !missingFromCache.includes(a))
2023-03-29 12:10:22 +00:00
.filter(a => (UserCache.getFromCache(a)?.loaded ?? 0) < expire);
2023-03-28 14:34:01 +00:00
const missing = new Set([...missingFromCache, ...expired]);
if (missing.size > 0) {
this.#log("Wants profiles: %d missing, %d expired", missingFromCache.length, expired.length);
2023-03-28 14:34:01 +00:00
const sub = new RequestBuilder("profiles");
2023-03-28 14:34:01 +00:00
sub
.withOptions({
skipDiff: true,
})
2023-03-28 14:34:01 +00:00
.withFilter()
.kinds([EventKind.SetMetadata])
.authors([...missing]);
const newProfiles = new Set<string>();
2023-05-30 13:48:38 +00:00
const q = this.#system.Query<PubkeyReplaceableNoteStore>(PubkeyReplaceableNoteStore, sub);
2023-06-01 08:54:25 +00:00
const feed = (q?.feed as PubkeyReplaceableNoteStore) ?? new PubkeyReplaceableNoteStore();
2023-03-28 14:34:01 +00:00
// never release this callback, it will stop firing anyway after eose
2023-06-01 08:54:25 +00:00
const releaseOnEvent = feed.onEvent(async e => {
for (const pe of e) {
newProfiles.add(pe.id);
await this.onProfileEvent(pe);
}
2023-04-25 17:01:29 +00:00
});
2023-03-28 14:34:01 +00:00
const results = await new Promise<Readonly<Array<TaggedRawEvent>>>(resolve => {
let timeout: ReturnType<typeof setTimeout> | undefined = undefined;
2023-06-01 08:54:25 +00:00
const release = feed.hook(() => {
if (!feed.loading) {
2023-03-28 14:34:01 +00:00
clearTimeout(timeout);
2023-06-01 08:54:25 +00:00
resolve(feed.getSnapshotData() ?? []);
this.#log("Profiles finished: %s", sub.id);
2023-04-25 17:01:29 +00:00
release();
2023-03-28 14:34:01 +00:00
}
});
timeout = setTimeout(() => {
release();
2023-06-01 08:54:25 +00:00
resolve(feed.getSnapshotData() ?? []);
this.#log("Profiles timeout: %s", sub.id);
2023-03-28 14:34:01 +00:00
}, 5_000);
});
2023-04-04 18:44:45 +00:00
releaseOnEvent();
2023-03-28 14:34:01 +00:00
const couldNotFetch = [...missing].filter(a => !results.some(b => b.pubkey === a));
if (couldNotFetch.length > 0) {
this.#log("No profiles: %o", couldNotFetch);
2023-03-28 14:34:01 +00:00
const empty = couldNotFetch.map(a =>
UserCache.update({
pubkey: a,
loaded: unixNowMs() - ProfileCacheExpire + 5_000, // expire in 5s
created: 69,
} as MetadataCache)
);
await Promise.all(empty);
}
// When we fetch an expired profile and its the same as what we already have
// onEvent is not fired and the loaded timestamp never gets updated
const expiredSame = results.filter(a => !newProfiles.has(a.id) && expired.includes(a.pubkey));
await Promise.all(expiredSame.map(v => this.onProfileEvent(v)));
2023-03-28 14:34:01 +00:00
}
setTimeout(() => this.#FetchMetadata(), 500);
}
}