import { ID, ReqFilter as Filter, STR, TaggedNostrEvent, UID } from "."; import loki from "lokijs"; import debug from "debug"; type PackedNostrEvent = { id: UID; pubkey: number; kind: number; tags: Array[]; flatTags: string[]; sig: string; created_at: number; content?: string; relays: string[]; saved_at: number; }; const DEFAULT_MAX_SIZE = 5000; class InMemoryDB { private loki = new loki("EventDB"); private eventsCollection: Collection; private maxSize: number; constructor(maxSize = DEFAULT_MAX_SIZE) { this.maxSize = maxSize; this.eventsCollection = this.loki.addCollection("events", { unique: ["id"], indices: ["pubkey", "kind", "flatTags", "created_at", "saved_at"], }); this.startRemoveOldestInterval(); } private startRemoveOldestInterval() { const removeOldest = () => { this.removeOldest(); setTimeout(() => removeOldest(), 3000); }; setTimeout(() => removeOldest(), 3000); } #log = debug("InMemoryDB"); get(id: string): TaggedNostrEvent | undefined { const event = this.eventsCollection.by("id", ID(id)); // throw if db not ready yet? if (event) { return this.unpack(event); } } has(id: string): boolean { return !!this.eventsCollection.by("id", ID(id)); } // map to internal UIDs to save memory private pack(event: TaggedNostrEvent): PackedNostrEvent { return { id: ID(event.id), pubkey: ID(event.pubkey), sig: event.sig, kind: event.kind, tags: event.tags.map(tag => { if (["e", "p"].includes(tag[0]) && typeof tag[1] === "string") { return [tag[0], ID(tag[1] as string), ...tag.slice(2)]; } else { return tag; } }), flatTags: event.tags.filter(tag => ["e", "p", "d"].includes(tag[0])).map(tag => `${tag[0]}_${ID(tag[1])}`), created_at: event.created_at, content: event.content, relays: event.relays, saved_at: Date.now(), }; } private unpack(packedEvent: PackedNostrEvent): TaggedNostrEvent { return { id: STR(packedEvent.id), pubkey: STR(packedEvent.pubkey), sig: packedEvent.sig, kind: packedEvent.kind, tags: packedEvent.tags.map(tag => { if (["e", "p"].includes(tag[0] as string) && typeof tag[1] === "number") { return [tag[0], STR(tag[1] as number), ...tag.slice(2)]; } else { return tag; } }), created_at: packedEvent.created_at, content: packedEvent.content, relays: packedEvent.relays, }; } handleEvent(event: TaggedNostrEvent): boolean { if (!event || !event.id || !event.created_at) { throw new Error("Invalid event"); } const id = ID(event.id); if (this.eventsCollection.by("id", id)) { return false; // this prevents updating event.relays? } const packed = this.pack(event); // we might want to limit the kinds of events we save, e.g. no kind 0, 3 or only 1, 6 try { this.eventsCollection.insert(packed); } catch (e) { return false; } return true; } remove(eventId: string): void { const id = ID(eventId); this.eventsCollection.findAndRemove({ id }); } removeOldest(): void { const count = this.eventsCollection.count(); this.#log("InMemoryDB: count", count, this.maxSize); if (count > this.maxSize) { this.#log("InMemoryDB: removing oldest events", count - this.maxSize); this.eventsCollection .chain() .simplesort("saved_at") .limit(count - this.maxSize) .remove(); } } find(filter: Filter, callback: (event: TaggedNostrEvent) => void): void { this.findArray(filter).forEach(event => { callback(event); }); } findArray(filter: Filter): TaggedNostrEvent[] { const query = this.constructQuery(filter); const searchRegex = filter.search ? new RegExp(filter.search, "i") : undefined; let chain = this.eventsCollection .chain() .find(query) .where((e: PackedNostrEvent) => { if (searchRegex && !e.content?.match(searchRegex)) { return false; } return true; }) .simplesort("created_at", true); if (filter.limit) { chain = chain.limit(filter.limit); } return chain.data().map(e => this.unpack(e)); } findAndRemove(filter: Filter) { const query = this.constructQuery(filter); this.eventsCollection.findAndRemove(query); } private constructQuery(filter: Filter): LokiQuery { const query: LokiQuery = {}; if (filter.ids) { query.id = { $in: filter.ids.map(ID) }; } else { if (filter.authors) { query.pubkey = { $in: filter.authors.map(ID) }; } if (filter.kinds) { query.kind = { $in: filter.kinds }; } if (filter["#e"]) { query.flatTags = { $contains: "e_" + filter["#e"]!.map(ID) }; } else if (filter["#p"]) { query.flatTags = { $contains: "p_" + filter["#p"]!.map(ID) }; } else if (filter["#d"]) { query.flatTags = { $contains: "d_" + filter["#d"]!.map(ID) }; } if (filter.since && filter.until) { query.created_at = { $between: [filter.since, filter.until] }; } if (filter.since) { query.created_at = { $gte: filter.since }; } if (filter.until) { query.created_at = { $lte: filter.until }; } } return query; } findOne(filter: Filter): TaggedNostrEvent | undefined { return this.findArray(filter)[0]; } } export { InMemoryDB }; export default new InMemoryDB();