snort/packages/system/src/sync/safe-sync.ts

143 lines
3.3 KiB
TypeScript

import EventEmitter from "eventemitter3";
import {
EventExt,
EventSigner,
EventType,
NostrEvent,
NostrLink,
NotSignedNostrEvent,
RequestBuilder,
SystemInterface,
} from "..";
import { unixNow } from "@snort/shared";
import debug from "debug";
export interface SafeSyncEvents {
change: () => void;
}
/**
* Safely sync replacable events to nostr
*
* Usefule for the following critical kinds:
* 0 (Metadata)
* 3 (Contacts)
* 10002 (Relays)
* 30078 (AppData)
*/
export class SafeSync extends EventEmitter<SafeSyncEvents> {
#log = debug("SafeSync");
#base: NostrEvent | undefined;
#didSync = false;
constructor(readonly link: NostrLink) {
super();
}
get value() {
return this.#base ? Object.freeze({ ...this.#base }) : undefined;
}
get didSync() {
return this.#didSync;
}
/**
* Fetch the latest version
*/
async sync(system: SystemInterface) {
if (this.link.kind === undefined || this.link.author === undefined) {
throw new Error("Kind must be set");
}
return await this.#sync(system);
}
/**
* Set the base value
* @param ev
*/
setBase(ev: NostrEvent) {
this.#checkForUpdate(ev, false);
this.#base = ev;
this.emit("change");
}
/**
* Publish an update for this event
*
* Event will be signed again inside
* @param ev
*/
async update(
next: NostrEvent | NotSignedNostrEvent,
signer: EventSigner,
system: SystemInterface,
mustExist?: boolean,
) {
if ("sig" in next) {
next.id = "";
next.sig = "";
}
console.debug(this.#base, next);
const signed = await this.#signEvent(next, signer);
// always attempt to get a newer version before broadcasting
await this.#sync(system);
this.#checkForUpdate(signed, mustExist ?? true);
system.BroadcastEvent(signed);
this.#base = signed;
this.emit("change");
}
async #signEvent(next: NotSignedNostrEvent, signer: EventSigner) {
const toSign = { ...next, id: "", sig: "" } as NostrEvent;
toSign.created_at = unixNow();
toSign.id = EventExt.createId(toSign);
return await signer.sign(toSign);
}
async #sync(system: SystemInterface) {
const rb = new RequestBuilder("sync");
const f = rb.withFilter().link(this.link);
if (this.#base) {
f.since(this.#base.created_at);
}
const results = await system.Fetch(rb);
const res = results.find(a => this.link.matchesEvent(a));
this.#log("Got result %O", res);
if (res && res.created_at > (this.#base?.created_at ?? 0)) {
this.#base = res;
this.emit("change");
}
this.#didSync = true;
return this.#base;
}
#checkForUpdate(ev: NostrEvent, mustExist: boolean) {
if (!this.#base) {
if (mustExist) {
throw new Error("No previous version detected");
} else {
return;
}
}
const prevTag = ev.tags.find(a => a[0] === "previous");
if (prevTag && prevTag[1] !== this.#base.id) {
throw new Error("Previous tag does not match our version");
}
if (
EventExt.getType(ev.kind) !== EventType.Replaceable &&
EventExt.getType(ev.kind) !== EventType.ParameterizedReplaceable
) {
throw new Error("Not a replacable event kind");
}
if (this.#base.created_at >= ev.created_at) {
throw new Error("Same version, cannot update");
}
}
}