forked from Kieran/snort
282 lines
7.0 KiB
TypeScript
282 lines
7.0 KiB
TypeScript
import { appendDedupe } from "@snort/shared";
|
|
import { EventExt, EventType, TaggedNostrEvent, u256 } from ".";
|
|
import { findTag } from "./utils";
|
|
|
|
export interface StoreSnapshot<TSnapshot> {
|
|
data: TSnapshot | undefined;
|
|
clear: () => void;
|
|
loading: () => boolean;
|
|
add: (ev: Readonly<TaggedNostrEvent> | Readonly<Array<TaggedNostrEvent>>) => void;
|
|
}
|
|
|
|
export const EmptySnapshot = {
|
|
data: undefined,
|
|
clear: () => {
|
|
// empty
|
|
},
|
|
loading: () => true,
|
|
add: () => {
|
|
// empty
|
|
},
|
|
} as StoreSnapshot<FlatNoteStore>;
|
|
|
|
export type NoteStoreSnapshotData = Array<TaggedNostrEvent> | TaggedNostrEvent;
|
|
export type NoteStoreHook = () => void;
|
|
export type NoteStoreHookRelease = () => void;
|
|
export type OnEventCallback = (e: Readonly<Array<TaggedNostrEvent>>) => void;
|
|
export type OnEventCallbackRelease = () => void;
|
|
export type OnEoseCallback = (c: string) => void;
|
|
export type OnEoseCallbackRelease = () => void;
|
|
|
|
/**
|
|
* Generic note store interface
|
|
*/
|
|
export abstract class NoteStore {
|
|
abstract add(ev: Readonly<TaggedNostrEvent> | Readonly<Array<TaggedNostrEvent>>): void;
|
|
abstract clear(): void;
|
|
|
|
// react hooks
|
|
abstract hook(cb: NoteStoreHook): NoteStoreHookRelease;
|
|
abstract getSnapshotData(): NoteStoreSnapshotData | undefined;
|
|
|
|
// events
|
|
abstract onEvent(cb: OnEventCallback): OnEventCallbackRelease;
|
|
|
|
abstract get snapshot(): StoreSnapshot<NoteStoreSnapshotData>;
|
|
abstract get loading(): boolean;
|
|
abstract set loading(v: boolean);
|
|
}
|
|
|
|
export abstract class HookedNoteStore<TSnapshot extends NoteStoreSnapshotData> implements NoteStore {
|
|
#hooks: Array<NoteStoreHook> = [];
|
|
#eventHooks: Array<OnEventCallback> = [];
|
|
#loading = true;
|
|
#storeSnapshot: StoreSnapshot<TSnapshot> = {
|
|
clear: () => this.clear(),
|
|
loading: () => this.loading,
|
|
add: ev => this.add(ev),
|
|
data: undefined,
|
|
};
|
|
#needsSnapshot = true;
|
|
#nextNotifyTimer?: ReturnType<typeof setTimeout>;
|
|
|
|
get snapshot() {
|
|
this.#updateSnapshot();
|
|
return this.#storeSnapshot;
|
|
}
|
|
|
|
get loading() {
|
|
return this.#loading;
|
|
}
|
|
|
|
set loading(v: boolean) {
|
|
this.#loading = v;
|
|
this.onChange([]);
|
|
}
|
|
|
|
abstract add(ev: Readonly<TaggedNostrEvent> | Readonly<Array<TaggedNostrEvent>>): void;
|
|
abstract clear(): void;
|
|
|
|
hook(cb: NoteStoreHook): NoteStoreHookRelease {
|
|
this.#hooks.push(cb);
|
|
return () => {
|
|
const idx = this.#hooks.findIndex(a => a === cb);
|
|
this.#hooks.splice(idx, 1);
|
|
};
|
|
}
|
|
|
|
getSnapshotData() {
|
|
this.#updateSnapshot();
|
|
return this.#storeSnapshot.data;
|
|
}
|
|
|
|
onEvent(cb: OnEventCallback): OnEventCallbackRelease {
|
|
const existing = this.#eventHooks.find(a => a === cb);
|
|
if (!existing) {
|
|
this.#eventHooks.push(cb);
|
|
return () => {
|
|
const idx = this.#eventHooks.findIndex(a => a === cb);
|
|
this.#eventHooks.splice(idx, 1);
|
|
};
|
|
}
|
|
return () => {
|
|
//noop
|
|
};
|
|
}
|
|
|
|
protected abstract takeSnapshot(): TSnapshot | undefined;
|
|
|
|
protected onChange(changes: Readonly<Array<TaggedNostrEvent>>): void {
|
|
this.#needsSnapshot = true;
|
|
if (!this.#nextNotifyTimer) {
|
|
this.#nextNotifyTimer = setTimeout(() => {
|
|
this.#nextNotifyTimer = undefined;
|
|
for (const hk of this.#hooks) {
|
|
hk();
|
|
}
|
|
}, 500);
|
|
}
|
|
if (changes.length > 0) {
|
|
for (const hkE of this.#eventHooks) {
|
|
hkE(changes);
|
|
}
|
|
}
|
|
}
|
|
|
|
#updateSnapshot() {
|
|
if (this.#needsSnapshot) {
|
|
this.#storeSnapshot = {
|
|
...this.#storeSnapshot,
|
|
data: this.takeSnapshot(),
|
|
};
|
|
this.#needsSnapshot = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A store which doesnt store anything, useful for hooks only
|
|
*/
|
|
export class NoopStore extends HookedNoteStore<Array<TaggedNostrEvent>> {
|
|
override add(ev: readonly TaggedNostrEvent[] | Readonly<TaggedNostrEvent>): void {
|
|
this.onChange(Array.isArray(ev) ? ev : [ev]);
|
|
}
|
|
|
|
override clear(): void {
|
|
// nothing to do
|
|
}
|
|
|
|
protected override takeSnapshot(): TaggedNostrEvent[] | undefined {
|
|
// nothing to do
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A simple flat container of events with no duplicates
|
|
*/
|
|
export class FlatNoteStore extends HookedNoteStore<Array<TaggedNostrEvent>> {
|
|
#events: Array<TaggedNostrEvent> = [];
|
|
#ids: Set<u256> = new Set();
|
|
|
|
add(ev: TaggedNostrEvent | Array<TaggedNostrEvent>) {
|
|
ev = Array.isArray(ev) ? ev : [ev];
|
|
const changes: Array<TaggedNostrEvent> = [];
|
|
ev.forEach(a => {
|
|
if (!this.#ids.has(a.id)) {
|
|
this.#events.push(a);
|
|
this.#ids.add(a.id);
|
|
changes.push(a);
|
|
} else {
|
|
const existing = this.#events.findIndex(b => b.id === a.id);
|
|
if (existing !== -1) {
|
|
this.#events[existing].relays = appendDedupe(this.#events[existing].relays, a.relays);
|
|
}
|
|
}
|
|
});
|
|
|
|
if (changes.length > 0) {
|
|
this.onChange(changes);
|
|
}
|
|
}
|
|
|
|
clear() {
|
|
this.#events = [];
|
|
this.#ids.clear();
|
|
this.onChange([]);
|
|
}
|
|
|
|
takeSnapshot() {
|
|
return [...this.#events];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A note store that holds a single replaceable event for a given user defined key generator function
|
|
*/
|
|
export class KeyedReplaceableNoteStore extends HookedNoteStore<Array<TaggedNostrEvent>> {
|
|
#keyFn: (ev: TaggedNostrEvent) => string;
|
|
#events: Map<string, TaggedNostrEvent> = new Map();
|
|
|
|
constructor(fn: (ev: TaggedNostrEvent) => string) {
|
|
super();
|
|
this.#keyFn = fn;
|
|
}
|
|
|
|
add(ev: TaggedNostrEvent | Array<TaggedNostrEvent>) {
|
|
ev = Array.isArray(ev) ? ev : [ev];
|
|
const changes: Array<TaggedNostrEvent> = [];
|
|
ev.forEach(a => {
|
|
const keyOnEvent = this.#keyFn(a);
|
|
const existingCreated = this.#events.get(keyOnEvent)?.created_at ?? 0;
|
|
if (a.created_at > existingCreated) {
|
|
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()];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A note store that holds a single replaceable event
|
|
*/
|
|
export class ReplaceableNoteStore extends HookedNoteStore<Readonly<TaggedNostrEvent>> {
|
|
#event?: TaggedNostrEvent;
|
|
|
|
add(ev: TaggedNostrEvent | Array<TaggedNostrEvent>) {
|
|
ev = Array.isArray(ev) ? ev : [ev];
|
|
const changes: Array<TaggedNostrEvent> = [];
|
|
ev.forEach(a => {
|
|
const existingCreated = this.#event?.created_at ?? 0;
|
|
if (a.created_at > existingCreated) {
|
|
this.#event = a;
|
|
changes.push(a);
|
|
}
|
|
});
|
|
if (changes.length > 0) {
|
|
this.onChange(changes);
|
|
}
|
|
}
|
|
|
|
clear() {
|
|
this.#event = undefined;
|
|
this.onChange([]);
|
|
}
|
|
|
|
takeSnapshot() {
|
|
if (this.#event) {
|
|
return { ...this.#event };
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
});
|
|
}
|
|
}
|