feat: UserState
This commit is contained in:
@ -1,100 +1,210 @@
|
||||
import { EventBuilder, EventSigner, NostrLink, SystemInterface } from "..";
|
||||
import { SafeSync } from "./safe-sync";
|
||||
import { EventEmitter } from "eventemitter3";
|
||||
import { EventBuilder, EventSigner, NostrEvent, NostrLink, NotSignedNostrEvent, SystemInterface, Tag } from "..";
|
||||
import { SafeSync, SafeSyncEvents } from "./safe-sync";
|
||||
import debug from "debug";
|
||||
|
||||
interface TagDiff {
|
||||
type: "add" | "remove" | "replace";
|
||||
type: "add" | "remove" | "replace" | "update";
|
||||
tag: Array<string> | Array<Array<string>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add/Remove tags from event
|
||||
*/
|
||||
export class DiffSyncTags {
|
||||
export class DiffSyncTags extends EventEmitter<SafeSyncEvents> {
|
||||
#log = debug("DiffSyncTags");
|
||||
#sync = new SafeSync();
|
||||
#sync: SafeSync;
|
||||
#changes: Array<TagDiff> = [];
|
||||
#changesEncrypted: Array<TagDiff> = [];
|
||||
#decryptedContent?: string;
|
||||
|
||||
constructor(readonly link: NostrLink) {}
|
||||
constructor(readonly link: NostrLink) {
|
||||
super();
|
||||
this.#sync = new SafeSync(link);
|
||||
this.#sync.on("change", () => {
|
||||
this.emit("change");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw storage event
|
||||
*/
|
||||
get value() {
|
||||
return this.#sync.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current tag set
|
||||
*/
|
||||
get tags() {
|
||||
const next = this.#nextEvent();
|
||||
return next.tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get decrypted content
|
||||
*/
|
||||
get encryptedTags() {
|
||||
if (this.#decryptedContent) {
|
||||
const tags = JSON.parse(this.#decryptedContent) as Array<Array<string>>;
|
||||
return tags;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a tag
|
||||
*/
|
||||
add(tag: Array<string> | Array<Array<string>>) {
|
||||
this.#changes.push({
|
||||
add(tag: Array<string> | Array<Array<string>>, encrypted = false) {
|
||||
(encrypted ? this.#changesEncrypted : this.#changes).push({
|
||||
type: "add",
|
||||
tag,
|
||||
});
|
||||
this.emit("change");
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a tag
|
||||
*/
|
||||
remove(tag: Array<string> | Array<Array<string>>) {
|
||||
this.#changes.push({
|
||||
remove(tag: Array<string> | Array<Array<string>>, encrypted = false) {
|
||||
(encrypted ? this.#changesEncrypted : this.#changes).push({
|
||||
type: "remove",
|
||||
tag,
|
||||
});
|
||||
this.emit("change");
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a tag (remove+add)
|
||||
*/
|
||||
update(tag: Array<string> | Array<Array<string>>, encrypted = false) {
|
||||
(encrypted ? this.#changesEncrypted : this.#changes).push({
|
||||
type: "update",
|
||||
tag,
|
||||
});
|
||||
this.emit("change");
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace all the tags
|
||||
*/
|
||||
replace(tag: Array<Array<string>>) {
|
||||
this.#changes.push({
|
||||
replace(tag: Array<Array<string>>, encrypted = false) {
|
||||
(encrypted ? this.#changesEncrypted : this.#changes).push({
|
||||
type: "replace",
|
||||
tag,
|
||||
});
|
||||
this.emit("change");
|
||||
}
|
||||
|
||||
async sync(signer: EventSigner, system: SystemInterface) {
|
||||
await this.#sync.sync(system);
|
||||
|
||||
if (
|
||||
this.#sync.value?.content &&
|
||||
this.#sync.value?.content.startsWith("[") &&
|
||||
this.#sync.value?.content.endsWith("]")
|
||||
) {
|
||||
const decrypted = await signer.nip4Decrypt(this.#sync.value.content, await signer.getPubKey());
|
||||
this.#decryptedContent = decrypted;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply changes and save
|
||||
*/
|
||||
async persist(signer: EventSigner, system: SystemInterface, content?: string) {
|
||||
const cloneChanges = [...this.#changes];
|
||||
this.#changes = [];
|
||||
if (!this.#sync.didSync) {
|
||||
await this.sync(signer, system);
|
||||
}
|
||||
|
||||
// always start with sync
|
||||
const res = await this.#sync.sync(this.link, system);
|
||||
const isNew = this.#sync.value === undefined;
|
||||
const next = this.#nextEvent(content);
|
||||
// content is populated as tags, encrypt it
|
||||
if (next.content.length > 0 && !content) {
|
||||
next.content = await signer.nip4Encrypt(next.content, await signer.getPubKey());
|
||||
}
|
||||
await this.#sync.update(next, signer, system, !isNew);
|
||||
}
|
||||
|
||||
#nextEvent(content?: string): NotSignedNostrEvent {
|
||||
if (content !== undefined && this.#changesEncrypted.length > 0) {
|
||||
throw new Error("Cannot have both encrypted tags and explicit content");
|
||||
}
|
||||
let isNew = false;
|
||||
let next = res ? { ...res } : undefined;
|
||||
let next = this.#sync.value ? { ...this.#sync.value } : undefined;
|
||||
if (!next) {
|
||||
const eb = new EventBuilder();
|
||||
eb.fromLink(this.link);
|
||||
next = eb.build();
|
||||
isNew = true;
|
||||
}
|
||||
if (content) {
|
||||
|
||||
// apply changes onto next
|
||||
this.#applyChanges(next.tags, this.#changes);
|
||||
if (this.#changesEncrypted.length > 0 && !content) {
|
||||
const encryptedTags = isNew ? [] : this.encryptedTags;
|
||||
this.#applyChanges(encryptedTags, this.#changesEncrypted);
|
||||
next.content = JSON.stringify(encryptedTags);
|
||||
} else if (content) {
|
||||
next.content = content;
|
||||
}
|
||||
|
||||
// apply changes onto next
|
||||
for (const change of cloneChanges) {
|
||||
for (const changeTag of Array.isArray(change.tag[0])
|
||||
? (change.tag as Array<Array<string>>)
|
||||
: [change.tag as Array<string>]) {
|
||||
const existing = next.tags.findIndex(a => a.every((b, i) => changeTag[i] === b));
|
||||
switch (change.type) {
|
||||
case "add": {
|
||||
return next;
|
||||
}
|
||||
|
||||
#applyChanges(tags: Array<Array<string>>, changes: Array<TagDiff>) {
|
||||
for (const change of changes) {
|
||||
if (change.tag.length === 0 && change.type !== "replace") continue;
|
||||
|
||||
switch (change.type) {
|
||||
case "add": {
|
||||
const changeTags = Array.isArray(change.tag[0])
|
||||
? (change.tag as Array<Array<string>>)
|
||||
: [change.tag as Array<string>];
|
||||
for (const changeTag of changeTags) {
|
||||
const existing = tags.findIndex(a => change.tag[0] === a[0] && change.tag[1] === a[1]);
|
||||
if (existing === -1) {
|
||||
next.tags.push(changeTag);
|
||||
tags.push(changeTag);
|
||||
} else {
|
||||
this.#log("Tag already exists: %O", changeTag);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "remove": {
|
||||
break;
|
||||
}
|
||||
case "remove": {
|
||||
const changeTags = Array.isArray(change.tag[0])
|
||||
? (change.tag as Array<Array<string>>)
|
||||
: [change.tag as Array<string>];
|
||||
for (const changeTag of changeTags) {
|
||||
const existing = tags.findIndex(a => change.tag[0] === a[0] && change.tag[1] === a[1]);
|
||||
if (existing !== -1) {
|
||||
next.tags.splice(existing, 1);
|
||||
tags.splice(existing, 1);
|
||||
} else {
|
||||
this.#log("Could not find tag to remove: %O", changeTag);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "update": {
|
||||
const changeTags = Array.isArray(change.tag[0])
|
||||
? (change.tag as Array<Array<string>>)
|
||||
: [change.tag as Array<string>];
|
||||
for (const changeTag of changeTags) {
|
||||
const existing = tags.findIndex(a => change.tag[0] === a[0] && change.tag[1] === a[1]);
|
||||
if (existing !== -1) {
|
||||
tags[existing] = changeTag;
|
||||
} else {
|
||||
this.#log("Could not find tag to update: %O", changeTag);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "replace": {
|
||||
tags.splice(0, tags.length);
|
||||
tags.push(...(change.tag as Array<Array<string>>));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.#sync.update(next, signer, system, !isNew);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,3 @@
|
||||
export interface HasId {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export * from "./safe-sync";
|
||||
export * from "./range-sync";
|
||||
export * from "./json-in-event-sync";
|
||||
|
@ -1,14 +1,9 @@
|
||||
import { SafeSync } from "./safe-sync";
|
||||
import { HasId } from ".";
|
||||
import { EventBuilder, EventSigner, NostrEvent, NostrLink, NostrPrefix, SystemInterface } from "..";
|
||||
import { SafeSync, SafeSyncEvents } from "./safe-sync";
|
||||
import { EventBuilder, EventSigner, NostrEvent, NostrLink, SystemInterface } from "..";
|
||||
import debug from "debug";
|
||||
import EventEmitter from "eventemitter3";
|
||||
|
||||
export interface JsonSyncEvents {
|
||||
change: () => void;
|
||||
}
|
||||
|
||||
export class JsonEventSync<T extends HasId> extends EventEmitter<JsonSyncEvents> {
|
||||
export class JsonEventSync<T> extends EventEmitter<SafeSyncEvents> {
|
||||
#log = debug("JsonEventSync");
|
||||
#sync: SafeSync;
|
||||
#json: T;
|
||||
@ -19,7 +14,7 @@ export class JsonEventSync<T extends HasId> extends EventEmitter<JsonSyncEvents>
|
||||
readonly encrypt: boolean,
|
||||
) {
|
||||
super();
|
||||
this.#sync = new SafeSync();
|
||||
this.#sync = new SafeSync(link);
|
||||
this.#json = initValue;
|
||||
|
||||
this.#sync.on("change", () => this.emit("change"));
|
||||
@ -31,7 +26,7 @@ export class JsonEventSync<T extends HasId> extends EventEmitter<JsonSyncEvents>
|
||||
}
|
||||
|
||||
async sync(signer: EventSigner, system: SystemInterface) {
|
||||
const res = await this.#sync.sync(this.link, system);
|
||||
const res = await this.#sync.sync(system);
|
||||
this.#log("Sync result %O", res);
|
||||
if (res) {
|
||||
if (this.encrypt) {
|
||||
@ -71,6 +66,5 @@ export class JsonEventSync<T extends HasId> extends EventEmitter<JsonSyncEvents>
|
||||
|
||||
await this.#sync.update(next, signer, system, !isNew);
|
||||
this.#json = val;
|
||||
this.#json.id = next.id;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,14 @@
|
||||
import EventEmitter from "eventemitter3";
|
||||
import { EventExt, EventSigner, EventType, NostrEvent, NostrLink, RequestBuilder, SystemInterface } from "..";
|
||||
import {
|
||||
EventExt,
|
||||
EventSigner,
|
||||
EventType,
|
||||
NostrEvent,
|
||||
NostrLink,
|
||||
NotSignedNostrEvent,
|
||||
RequestBuilder,
|
||||
SystemInterface,
|
||||
} from "..";
|
||||
import { unixNow } from "@snort/shared";
|
||||
import debug from "debug";
|
||||
|
||||
@ -21,6 +30,10 @@ export class SafeSync extends EventEmitter<SafeSyncEvents> {
|
||||
#base: NostrEvent | undefined;
|
||||
#didSync = false;
|
||||
|
||||
constructor(readonly link: NostrLink) {
|
||||
super();
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.#base ? Object.freeze({ ...this.#base }) : undefined;
|
||||
}
|
||||
@ -31,14 +44,13 @@ export class SafeSync extends EventEmitter<SafeSyncEvents> {
|
||||
|
||||
/**
|
||||
* Fetch the latest version
|
||||
* @param link A link to the kind
|
||||
*/
|
||||
async sync(link: NostrLink, system: SystemInterface) {
|
||||
if (link.kind === undefined || link.author === undefined) {
|
||||
async sync(system: SystemInterface) {
|
||||
if (this.link.kind === undefined || this.link.author === undefined) {
|
||||
throw new Error("Kind must be set");
|
||||
}
|
||||
|
||||
return await this.#sync(link, system);
|
||||
return await this.#sync(system);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -57,15 +69,23 @@ export class SafeSync extends EventEmitter<SafeSyncEvents> {
|
||||
* Event will be signed again inside
|
||||
* @param ev
|
||||
*/
|
||||
async update(next: NostrEvent, signer: EventSigner, system: SystemInterface, mustExist?: boolean) {
|
||||
next.id = "";
|
||||
next.sig = "";
|
||||
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);
|
||||
const link = NostrLink.fromEvent(signed);
|
||||
|
||||
// always attempt to get a newer version before broadcasting
|
||||
await this.#sync(link, system);
|
||||
await this.#sync(system);
|
||||
this.#checkForUpdate(signed, mustExist ?? true);
|
||||
|
||||
system.BroadcastEvent(signed);
|
||||
@ -73,28 +93,21 @@ export class SafeSync extends EventEmitter<SafeSyncEvents> {
|
||||
this.emit("change");
|
||||
}
|
||||
|
||||
async #signEvent(next: NostrEvent, signer: EventSigner) {
|
||||
next.created_at = unixNow();
|
||||
if (this.#base) {
|
||||
const prevTag = next.tags.find(a => a[0] === "previous");
|
||||
if (prevTag) {
|
||||
prevTag[1] = this.#base.id;
|
||||
} else {
|
||||
next.tags.push(["previous", this.#base.id]);
|
||||
}
|
||||
}
|
||||
next.id = EventExt.createId(next);
|
||||
return await signer.sign(next);
|
||||
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(link: NostrLink, system: SystemInterface) {
|
||||
const rb = new RequestBuilder(`sync:${link.encode()}`);
|
||||
const f = rb.withFilter().link(link);
|
||||
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 => link.matchesEvent(a));
|
||||
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;
|
||||
|
Reference in New Issue
Block a user