import { SortedMap, dedupe } from "@snort/shared"; import { EventExt, EventType, TaggedNostrEvent } from "."; import { findTag } from "./utils"; import { EventEmitter } from "eventemitter3"; export const EmptySnapshot: NoteStoreSnapshotData = []; export type NoteStoreSnapshotData = Array; export type NoteStoreHook = () => void; export type NoteStoreHookRelease = () => void; export type OnEventCallback = (e: Readonly>) => void; export type OnEventCallbackRelease = () => void; export type OnEoseCallback = (c: string) => void; export type OnEoseCallbackRelease = () => void; export interface NosteStoreEvents { event: (evs: Array) => void; } /** * Generic note store interface */ export abstract class NoteStore extends EventEmitter { abstract add(ev: Readonly | Readonly>): void; abstract clear(): void; abstract get snapshot(): NoteStoreSnapshotData; } export abstract class HookedNoteStore extends NoteStore { #storeSnapshot: NoteStoreSnapshotData = []; #nextEmit?: ReturnType; #bufEmit: Array = []; get snapshot() { return this.#storeSnapshot; } abstract override add(ev: Readonly | Readonly>): void; abstract override clear(): void; protected abstract takeSnapshot(): NoteStoreSnapshotData | undefined; protected onChange(changes: Array): void { this.#storeSnapshot = this.takeSnapshot() ?? []; this.#bufEmit.push(...changes); if (!this.#nextEmit) { this.#nextEmit = setTimeout(() => { this.#nextEmit = undefined; this.emit("event", this.#bufEmit); this.#bufEmit = []; }, 300); } } } /** * A note store that holds a single replaceable event for a given user defined key generator function */ export class KeyedReplaceableNoteStore extends HookedNoteStore { #keyFn: (ev: TaggedNostrEvent) => string; #events: SortedMap = new SortedMap([], (a, b) => b[1].created_at - a[1].created_at); constructor(fn: (ev: TaggedNostrEvent) => string) { super(); this.#keyFn = fn; } add(ev: TaggedNostrEvent | Array) { ev = Array.isArray(ev) ? ev : [ev]; const changes: Array = []; ev.forEach(a => { const keyOnEvent = this.#keyFn(a); const existing = this.#events.get(keyOnEvent); const existingCreated = existing?.created_at ?? 0; if (a.created_at > existingCreated) { if (existing) { a.relays = dedupe([...existing.relays, ...a.relays]); } this.#events.set(keyOnEvent, a); changes.push(a); } }); if (changes.length > 0) { this.onChange(changes); } } clear() { this.#events.clear(); this.onChange([]); } takeSnapshot() { return [...this.#events.values()]; } } /** * General use note store based on kind ranges */ export class NoteCollection extends KeyedReplaceableNoteStore { constructor() { super(e => { switch (EventExt.getType(e.kind)) { case EventType.ParameterizedReplaceable: return `${e.kind}:${e.pubkey}:${findTag(e, "d")}`; case EventType.Replaceable: return `${e.kind}:${e.pubkey}`; default: return e.id; } }); } }