2023-06-15 11:03:05 +00:00
|
|
|
import debug from "debug";
|
|
|
|
import { unixNowMs, FeedCache } from "@snort/shared";
|
2023-08-17 18:54:14 +00:00
|
|
|
import { EventKind, HexKey, SystemInterface, TaggedNostrEvent, NoteCollection, RequestBuilder } from ".";
|
2023-06-21 15:48:35 +00:00
|
|
|
import { ProfileCacheExpire } from "./const";
|
|
|
|
import { mapEventToProfile, MetadataCache } from "./cache";
|
2023-03-28 14:34:01 +00:00
|
|
|
|
2023-07-22 18:37:46 +00:00
|
|
|
const MetadataRelays = ["wss://purplepag.es"];
|
2023-06-19 09:17:09 +00:00
|
|
|
|
2023-05-30 13:48:38 +00:00
|
|
|
export class ProfileLoaderService {
|
2023-06-01 08:54:25 +00:00
|
|
|
#system: SystemInterface;
|
2023-06-15 11:03:05 +00:00
|
|
|
#cache: FeedCache<MetadataCache>;
|
2023-05-30 13:48:38 +00:00
|
|
|
|
2023-06-19 09:17:09 +00:00
|
|
|
/**
|
|
|
|
* A set of pubkeys we could not find last run,
|
|
|
|
* This list will attempt to use known profile metadata relays
|
|
|
|
*/
|
|
|
|
#missingLastRun: Set<string> = new Set();
|
|
|
|
|
2023-03-28 14:34:01 +00:00
|
|
|
/**
|
|
|
|
* List of pubkeys to fetch metadata for
|
|
|
|
*/
|
2023-06-20 13:15:33 +00:00
|
|
|
#wantsMetadata: Set<HexKey> = new Set();
|
2023-03-28 14:34:01 +00:00
|
|
|
|
2023-05-24 23:03:54 +00:00
|
|
|
readonly #log = debug("ProfileCache");
|
|
|
|
|
2023-06-15 11:03:05 +00:00
|
|
|
constructor(system: SystemInterface, cache: FeedCache<MetadataCache>) {
|
2023-05-30 13:48:38 +00:00
|
|
|
this.#system = system;
|
2023-06-08 10:45:23 +00:00
|
|
|
this.#cache = cache;
|
2023-03-28 14:34:01 +00:00
|
|
|
this.#FetchMetadata();
|
|
|
|
}
|
|
|
|
|
2023-06-16 19:31:33 +00:00
|
|
|
get Cache() {
|
|
|
|
return this.#cache;
|
|
|
|
}
|
|
|
|
|
2023-03-28 14:34:01 +00:00
|
|
|
/**
|
|
|
|
* Request profile metadata for a set of pubkeys
|
|
|
|
*/
|
|
|
|
TrackMetadata(pk: HexKey | Array<HexKey>) {
|
|
|
|
for (const p of Array.isArray(pk) ? pk : [pk]) {
|
2023-09-19 12:03:29 +00:00
|
|
|
if (p.length === 64) {
|
2023-09-19 12:04:19 +00:00
|
|
|
this.#wantsMetadata.add(p);
|
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) {
|
2023-06-20 13:15:33 +00:00
|
|
|
this.#wantsMetadata.delete(p);
|
2023-03-28 14:34:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-17 18:54:14 +00:00
|
|
|
async onProfileEvent(e: Readonly<TaggedNostrEvent>) {
|
2023-05-24 23:03:54 +00:00
|
|
|
const profile = mapEventToProfile(e);
|
|
|
|
if (profile) {
|
2023-06-08 10:45:23 +00:00
|
|
|
await this.#cache.update(profile);
|
2023-04-04 18:44:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-14 11:31:17 +00:00
|
|
|
async fetchProfile(key: string) {
|
|
|
|
const existing = this.Cache.get(key);
|
|
|
|
if (existing) {
|
|
|
|
return existing;
|
|
|
|
} else {
|
|
|
|
return await new Promise<MetadataCache>((resolve, reject) => {
|
|
|
|
this.TrackMetadata(key);
|
|
|
|
const release = this.Cache.hook(() => {
|
|
|
|
const existing = this.Cache.getFromCache(key);
|
|
|
|
if (existing) {
|
|
|
|
resolve(existing);
|
|
|
|
release();
|
|
|
|
this.UntrackMetadata(key);
|
|
|
|
}
|
|
|
|
}, key);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-28 14:34:01 +00:00
|
|
|
async #FetchMetadata() {
|
2023-06-20 13:15:33 +00:00
|
|
|
const missingFromCache = await this.#cache.buffer([...this.#wantsMetadata]);
|
2023-03-28 14:34:01 +00:00
|
|
|
|
|
|
|
const expire = unixNowMs() - ProfileCacheExpire;
|
2023-06-20 13:15:33 +00:00
|
|
|
const expired = [...this.#wantsMetadata]
|
2023-03-28 14:34:01 +00:00
|
|
|
.filter(a => !missingFromCache.includes(a))
|
2023-06-08 10:45:23 +00:00
|
|
|
.filter(a => (this.#cache.getFromCache(a)?.loaded ?? 0) < expire);
|
2023-03-28 14:34:01 +00:00
|
|
|
const missing = new Set([...missingFromCache, ...expired]);
|
|
|
|
if (missing.size > 0) {
|
2023-05-24 23:03:54 +00:00
|
|
|
this.#log("Wants profiles: %d missing, %d expired", missingFromCache.length, expired.length);
|
2023-03-28 14:34:01 +00:00
|
|
|
|
2023-05-24 23:03:54 +00:00
|
|
|
const sub = new RequestBuilder("profiles");
|
2023-03-28 14:34:01 +00:00
|
|
|
sub
|
2023-05-24 23:03:54 +00:00
|
|
|
.withOptions({
|
|
|
|
skipDiff: true,
|
|
|
|
})
|
2023-03-28 14:34:01 +00:00
|
|
|
.withFilter()
|
|
|
|
.kinds([EventKind.SetMetadata])
|
|
|
|
.authors([...missing]);
|
|
|
|
|
2023-06-19 09:17:09 +00:00
|
|
|
if (this.#missingLastRun.size > 0) {
|
2023-07-22 18:37:46 +00:00
|
|
|
const fMissing = sub
|
|
|
|
.withFilter()
|
2023-06-19 09:17:09 +00:00
|
|
|
.kinds([EventKind.SetMetadata])
|
|
|
|
.authors([...this.#missingLastRun]);
|
|
|
|
MetadataRelays.forEach(r => fMissing.relay(r));
|
|
|
|
}
|
2023-05-24 23:03:54 +00:00
|
|
|
const newProfiles = new Set<string>();
|
2023-07-04 10:33:40 +00:00
|
|
|
const q = this.#system.Query<NoteCollection>(NoteCollection, sub);
|
|
|
|
const feed = (q?.feed as NoteCollection) ?? new NoteCollection();
|
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 => {
|
2023-05-24 23:03:54 +00:00
|
|
|
for (const pe of e) {
|
|
|
|
newProfiles.add(pe.id);
|
|
|
|
await this.onProfileEvent(pe);
|
|
|
|
}
|
2023-04-25 17:01:29 +00:00
|
|
|
});
|
2023-08-17 18:54:14 +00:00
|
|
|
const results = await new Promise<Readonly<Array<TaggedNostrEvent>>>(resolve => {
|
2023-03-28 14:34:01 +00:00
|
|
|
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() ?? []);
|
2023-05-24 23:03:54 +00:00
|
|
|
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() ?? []);
|
2023-05-24 23:03:54 +00:00
|
|
|
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));
|
2023-06-19 09:17:09 +00:00
|
|
|
this.#missingLastRun = new Set(couldNotFetch);
|
2023-03-28 14:34:01 +00:00
|
|
|
if (couldNotFetch.length > 0) {
|
2023-05-24 23:03:54 +00:00
|
|
|
this.#log("No profiles: %o", couldNotFetch);
|
2023-03-28 14:34:01 +00:00
|
|
|
const empty = couldNotFetch.map(a =>
|
2023-06-08 10:45:23 +00:00
|
|
|
this.#cache.update({
|
2023-03-28 14:34:01 +00:00
|
|
|
pubkey: a,
|
2023-07-23 22:19:26 +00:00
|
|
|
loaded: unixNowMs() - ProfileCacheExpire + 30_000, // expire in 30s
|
2023-03-28 14:34:01 +00:00
|
|
|
created: 69,
|
2023-07-24 14:30:21 +00:00
|
|
|
} as MetadataCache),
|
2023-03-28 14:34:01 +00:00
|
|
|
);
|
|
|
|
await Promise.all(empty);
|
|
|
|
}
|
2023-05-24 23:03:54 +00:00
|
|
|
|
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
}
|