From 270a4277678a374568a6747fdf37d168f3c75b10 Mon Sep 17 00:00:00 2001 From: BlowaterNostr <127284497+BlowaterNostr@users.noreply.github.com> Date: Sat, 25 Nov 2023 22:29:50 +0800 Subject: [PATCH] fix order issue (#336) --- UI/app.tsx | 13 +++-- UI/app_update.tsx | 10 +++- UI/config-other.test.ts | 51 +++++++++++------ UI/config-other.ts | 121 ++++++++++++++++++++++++++-------------- deno.lock | 3 + lib/nostr-ts | 2 +- nostr.ts | 12 ++++ time.ts | 10 ++-- 8 files changed, 148 insertions(+), 74 deletions(-) diff --git a/UI/app.tsx b/UI/app.tsx index 0b8bffc..6e20849 100644 --- a/UI/app.tsx +++ b/UI/app.tsx @@ -30,10 +30,12 @@ import { ProfileGetter } from "./search.tsx"; import { DirectedMessageController } from "../features/dm.ts"; import { ConnectionPool } from "../lib/nostr-ts/relay-pool.ts"; import { LamportTime } from "../time.ts"; +import { lastPathSegment } from "https://deno.land/std@0.186.0/path/_util.ts"; export async function Start(database: DexieDatabase) { console.log("Start the application"); + const lamport = new LamportTime(); const model = initialModel(); const eventBus = new EventBus(); const pool = new ConnectionPool(); @@ -53,7 +55,7 @@ export async function Start(database: DexieDatabase) { if (ctx instanceof Error) { console.error(ctx); } else if (ctx) { - const otherConfig = await OtherConfig.FromLocalStorage(ctx, newNostrEventChannel); + const otherConfig = await OtherConfig.FromLocalStorage(ctx, newNostrEventChannel, lamport); const app = await App.Start({ database: dbView, model, @@ -62,6 +64,7 @@ export async function Start(database: DexieDatabase) { pool, popOverInputChan, otherConfig, + lamport, }); model.app = app; } @@ -84,6 +87,7 @@ export async function Start(database: DexieDatabase) { pool, popOver: popOverInputChan, newNostrEventChannel: newNostrEventChannel, + lamport, }) ) { const t = Date.now(); @@ -128,8 +132,9 @@ export class App { pool: ConnectionPool; popOverInputChan: PopOverInputChannel; otherConfig: OtherConfig; + lamport: LamportTime; }) { - const lamport = LamportTime.FromEvents(args.database.getAllEvents()); + args.lamport.fromEvents(args.database.getAllEvents()); const eventSyncer = new EventSyncer(args.pool, args.database); // init relay config @@ -209,7 +214,7 @@ export class App { conversationLists, relayConfig, groupChatController, - lamport, + args.lamport, dmController, ); await app.initApp(); @@ -223,7 +228,7 @@ export class App { (async () => { const stream = await this.pool.newSub(OtherConfig.name, { authors: [this.ctx.publicKey.hex], - kinds: [NostrKind.Custom_App_Data], + kinds: [NostrKind.Encrypted_Custom_App_Data], }); if (stream instanceof Error) { throw stream; // crash the app diff --git a/UI/app_update.tsx b/UI/app_update.tsx index 20e6918..e5bdebd 100644 --- a/UI/app_update.tsx +++ b/UI/app_update.tsx @@ -81,6 +81,7 @@ export async function* UI_Interaction_Update(args: { pool: ConnectionPool; popOver: PopOverInputChannel; newNostrEventChannel: Channel; + lamport: LamportTime; }) { const { model, dbView, eventBus, pool } = args; const events = eventBus.onChange(); @@ -91,7 +92,11 @@ export async function* UI_Interaction_Update(args: { const ctx = event.ctx; if (ctx) { console.log("sign in as", ctx.publicKey.bech32()); - const otherConfig = await OtherConfig.FromLocalStorage(ctx, args.newNostrEventChannel); + const otherConfig = await OtherConfig.FromLocalStorage( + ctx, + args.newNostrEventChannel, + args.lamport, + ); const app = await App.Start({ database: dbView, model, @@ -100,6 +105,7 @@ export async function* UI_Interaction_Update(args: { pool, popOverInputChan: args.popOver, otherConfig, + lamport: args.lamport, }); model.app = app; } else { @@ -545,7 +551,7 @@ export async function* Database_Update( await database.remove(e.id); } } - } else if (e.kind == NostrKind.Custom_App_Data) { + } else if (e.kind == NostrKind.Encrypted_Custom_App_Data) { console.log(e); const err = await args.otherConfig.addEvent(e); if (err instanceof Error) { diff --git a/UI/config-other.test.ts b/UI/config-other.test.ts index d6be372..731d8f8 100644 --- a/UI/config-other.test.ts +++ b/UI/config-other.test.ts @@ -2,12 +2,14 @@ import { assertEquals, fail } from "https://deno.land/std@0.176.0/testing/assert import { OtherConfig } from "./config-other.ts"; import { InMemoryAccountContext, NostrEvent } from "../lib/nostr-ts/nostr.ts"; import { Channel } from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts"; +import { LamportTime } from "../time.ts"; Deno.test("Pin List", async () => { const ctx = InMemoryAccountContext.Generate(); const pusher = new Channel(); const _ = new Channel(); - const config = OtherConfig.Empty(pusher, ctx); + const lamport = new LamportTime(); + const config = OtherConfig.Empty(pusher, ctx, lamport); await config.addPin("a"); assertEquals(config.getPinList(), new Set(["a"])); @@ -16,34 +18,47 @@ Deno.test("Pin List", async () => { assertEquals(config.getPinList(), new Set(["a", "b"])); // able to restore the config from local storage - const config2 = await OtherConfig.FromLocalStorage(ctx, _); - assertEquals(config2.getPinList(), new Set(["a", "b"])); - assertEquals(config2.getPinList(), config.getPinList()); - - // able to restore the config from event logs - const config3 = OtherConfig.Empty(_, ctx); - const event1 = await pusher.pop() as NostrEvent; - const event2 = await pusher.pop() as NostrEvent; { - const err = await config3.addEvent(event1); - if (err instanceof Error) fail(err.message); + const config2 = await OtherConfig.FromLocalStorage(ctx, _, lamport); + assertEquals(config2.getPinList(), new Set(["a", "b"])); + assertEquals(config2.getPinList(), config.getPinList()); } - { - const err = await config3.addEvent(event2); - if (err instanceof Error) fail(err.message); - } - assertEquals(config3.getPinList(), new Set(["a", "b"])); - assertEquals(config3.getPinList(), config.getPinList()); // remove 1 pin from config1 await config.removePin("a"); assertEquals(config.getPinList(), new Set(["b"])); // config3 is able to sync with config1 - const event3 = await pusher.pop() as NostrEvent; + // able to restore the config from event logs + const config3 = OtherConfig.Empty(_, ctx, lamport); + const event1 = await pusher.pop() as NostrEvent; // +a + const event2 = await pusher.pop() as NostrEvent; // +b + const event3 = await pusher.pop() as NostrEvent; // -a + + { + const err = await config3.addEvent(event2); + if (err instanceof Error) fail(err.message); + } + assertEquals(config3.getPinList(), new Set(["b"])); + + // apply -a before +a { const err = await config3.addEvent(event3); if (err instanceof Error) fail(err.message); } + { + const err = await config3.addEvent(event1); + if (err instanceof Error) fail(err.message); + } assertEquals(config3.getPinList(), new Set(["b"])); + + // +a again + await config.addPin("a"); + assertEquals(config.getPinList(), new Set(["a", "b"])); + const event4 = await pusher.pop() as NostrEvent; + { + const err = await config3.addEvent(event4); + if (err instanceof Error) fail(err.message); + assertEquals(config3.getPinList(), new Set(["a", "b"])); + } }); diff --git a/UI/config-other.ts b/UI/config-other.ts index ce8b521..091bfd8 100644 --- a/UI/config-other.ts +++ b/UI/config-other.ts @@ -3,77 +3,102 @@ import { NostrAccountContext, NostrEvent, NostrKind, verifyEvent } from "../lib/ import { PinListGetter } from "./conversation-list.tsx"; import { parseJSON } from "../features/profile.ts"; import { Channel } from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts"; -import { PinConversation, UnpinConversation } from "../nostr.ts"; +import { + PinConversation, + PinConversationRelay, + UnpinConversation, + UnpinConversationRelay, +} from "../nostr.ts"; +import { LamportTime } from "../time.ts"; export type NostrEventAdder = { addEvent(event: NostrEvent): Promise; }; export class OtherConfig implements PinListGetter, NostrEventAdder { - static Empty(nostrEventPusher: Channel, ctx: NostrAccountContext) { - return new OtherConfig(nostrEventPusher, ctx); + static Empty(nostrEventPusher: Channel, ctx: NostrAccountContext, lamport: LamportTime) { + return new OtherConfig(nostrEventPusher, ctx, lamport); } - static async FromLocalStorage(ctx: NostrAccountContext, eventPusher: Channel) { + static async FromLocalStorage( + ctx: NostrAccountContext, + eventPusher: Channel, + lamport: LamportTime, + ) { const item = localStorage.getItem(`${OtherConfig.name}:${ctx.publicKey.bech32()}`); if (item == null) { - return OtherConfig.Empty(eventPusher, ctx); + return OtherConfig.Empty(eventPusher, ctx, lamport); } const event = parseJSON(item); if (event instanceof Error) { console.error(event); - return OtherConfig.Empty(eventPusher, ctx); + return OtherConfig.Empty(eventPusher, ctx, lamport); } const ok = await verifyEvent(event); if (!ok) { - return OtherConfig.Empty(eventPusher, ctx); + return OtherConfig.Empty(eventPusher, ctx, lamport); } - if (event.kind == NostrKind.Custom_App_Data) { + if (event.kind == NostrKind.Encrypted_Custom_App_Data) { const config = await OtherConfig.FromNostrEvent( - // @ts-ignore - event, + { + ...event, + kind: event.kind, + }, ctx, eventPusher, + lamport, ); if (config instanceof Error) { - return OtherConfig.Empty(eventPusher, ctx); + return OtherConfig.Empty(eventPusher, ctx, lamport); } return config; } - return OtherConfig.Empty(eventPusher, ctx); + return OtherConfig.Empty(eventPusher, ctx, lamport); } private constructor( private readonly nostrEventPusher: Channel, private readonly ctx: NostrAccountContext, + private readonly lamport: LamportTime, ) {} - private pinList = new Set(); // set of pubkeys in npub format + private pinList = new Map(); // set of pubkeys in npub format getPinList(): Set { - return this.pinList; + const set = new Set(); + for (const event of this.pinList.values()) { + if (event.type == "PinConversation") { + set.add(event.pubkey); + } + } + return set; } async addPin(pubkey: string) { - if (this.pinList.has(pubkey)) { + const currentPin = this.pinList.get(pubkey); + if (currentPin && currentPin.type == "PinConversation") { return; } - this.pinList.add(pubkey); - const err = await this.saveToLocalStorage(); - if (err instanceof Error) { - return err; - } + + const pin: PinConversationRelay = { + pubkey, + type: "PinConversation", + lamport: this.lamport.now(), + }; + const event = await prepareEncryptedNostrEvent(this.ctx, { - content: JSON.stringify({ - type: "PinConversation", - pubkey: pubkey, - }), + content: JSON.stringify(pin), encryptKey: this.ctx.publicKey, - kind: NostrKind.Custom_App_Data, + kind: NostrKind.Encrypted_Custom_App_Data, }); if (event instanceof Error) { return event; } + this.pinList.set(pubkey, pin); + const err = await this.saveToLocalStorage(); + if (err instanceof Error) { + return err; + } /* no await */ this.nostrEventPusher.put(event); } @@ -82,24 +107,33 @@ export class OtherConfig implements PinListGetter, NostrEventAdder { if (!exist) { return; } + + const unpin: UnpinConversationRelay = { + pubkey, + type: "UnpinConversation", + lamport: this.lamport.now(), + }; const event = await prepareEncryptedNostrEvent(this.ctx, { - content: JSON.stringify({ - type: "UnpinConversation", - pubkey: pubkey, - }), + content: JSON.stringify(unpin), encryptKey: this.ctx.publicKey, - kind: NostrKind.Custom_App_Data, + kind: NostrKind.Encrypted_Custom_App_Data, }); if (event instanceof Error) { return event; } + this.pinList.set(pubkey, unpin); + const err = await this.saveToLocalStorage(); + if (err instanceof Error) { + return err; + } /* no await */ this.nostrEventPusher.put(event); } static async FromNostrEvent( - event: NostrEvent, + event: NostrEvent, ctx: NostrAccountContext, pusher: Channel, + lamport: LamportTime, ) { const decrypted = await ctx.decrypt(ctx.publicKey.hex, event.content); if (decrypted instanceof Error) { @@ -118,7 +152,7 @@ export class OtherConfig implements PinListGetter, NostrEventAdder { pinList = []; } - const c = new OtherConfig(pusher, ctx); + const c = new OtherConfig(pusher, ctx, lamport); for (const pin of pinList) { const err = await c.addPin(pin); if (err instanceof Error) { @@ -131,8 +165,8 @@ export class OtherConfig implements PinListGetter, NostrEventAdder { private async toNostrEvent(ctx: NostrAccountContext) { const event = await prepareEncryptedNostrEvent(ctx, { encryptKey: ctx.publicKey, - content: JSON.stringify(Array.from(this.pinList)), - kind: NostrKind.Custom_App_Data, + content: JSON.stringify(Array.from(this.getPinList())), + kind: NostrKind.Encrypted_Custom_App_Data, tags: [], }); return event; @@ -147,7 +181,7 @@ export class OtherConfig implements PinListGetter, NostrEventAdder { } async addEvent(event: NostrEvent) { - if (event.kind != NostrKind.Custom_App_Data) { + if (event.kind != NostrKind.Encrypted_Custom_App_Data) { return; } const decrypted = await this.ctx.decrypt(this.ctx.publicKey.hex, event.content); @@ -160,14 +194,15 @@ export class OtherConfig implements PinListGetter, NostrEventAdder { } if (pin.type == "PinConversation" || pin.type == "UnpinConversation") { - if (pin.type == "PinConversation") { - if (this.pinList.has(pin.pubkey)) { - return; - } - this.pinList.add(pin.pubkey); - } else { - this.pinList.delete(pin.pubkey); + const currentEvent = this.pinList.get(pin.pubkey); + + if (currentEvent && pin.lamport < currentEvent.lamport) { + return; // ignore because the current event is newer } + + this.lamport.set(pin.lamport); + this.pinList.set(pin.pubkey, pin); + const err = await this.saveToLocalStorage(); if (err instanceof Error) { return err; @@ -176,4 +211,4 @@ export class OtherConfig implements PinListGetter, NostrEventAdder { } } -export type ConfigEvent = PinConversation | UnpinConversation; +export type ConfigEvent = PinConversationRelay | UnpinConversationRelay; diff --git a/deno.lock b/deno.lock index 2a7202f..cc2fee1 100644 --- a/deno.lock +++ b/deno.lock @@ -6,6 +6,9 @@ "https://deno.land/std@0.176.0/testing/_diff.ts": "1a3c044aedf77647d6cac86b798c6417603361b66b54c53331b312caeb447aea", "https://deno.land/std@0.176.0/testing/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7", "https://deno.land/std@0.176.0/testing/asserts.ts": "984ab0bfb3faeed92ffaa3a6b06536c66811185328c5dd146257c702c41b01ab", + "https://deno.land/std@0.186.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", + "https://deno.land/std@0.186.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", + "https://deno.land/std@0.186.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0", "https://esm.sh/@noble/hashes@1.3.2/utils": "20c519683900b5873b16ff15377049f6e86e183b612a0e442f6acbc056667e6a", "https://esm.sh/@scure/bip32@1.3.2": "8f8111ae2b0865644daf69d6b0d8ea76bb15112453f5dc697ba29b44b527b26c", "https://esm.sh/@scure/bip39@1.2.1": "7d6cdfce191281c81406e55de5216714dd22b19790123b652421bbf9718e5057", diff --git a/lib/nostr-ts b/lib/nostr-ts index 8afbf39..be759ea 160000 --- a/lib/nostr-ts +++ b/lib/nostr-ts @@ -1 +1 @@ -Subproject commit 8afbf3999af91d9f7320789509abbfdb1405d17c +Subproject commit be759ead56a6eb8984e024296f79506b42c89199 diff --git a/nostr.ts b/nostr.ts index db39c14..2918ef7 100644 --- a/nostr.ts +++ b/nostr.ts @@ -58,11 +58,23 @@ export type PinConversation = { pubkey: string; }; +export type PinConversationRelay = { + type: "PinConversation"; + pubkey: string; + lamport: number; +}; + export type UnpinConversation = { type: "UnpinConversation"; pubkey: string; }; +export type UnpinConversationRelay = { + type: "UnpinConversation"; + pubkey: string; + lamport: number; +}; + export type UserLogin = { type: "UserLogin"; }; diff --git a/time.ts b/time.ts index 001bbd7..3792f98 100644 --- a/time.ts +++ b/time.ts @@ -2,17 +2,15 @@ import { NostrEvent } from "./lib/nostr-ts/nostr.ts"; import { getTags } from "./nostr.ts"; export class LamportTime { - constructor(private time: number) {} + private time = 0; - static FromEvents(events: Iterable) { - let time = 0; + fromEvents(events: Iterable) { for (const event of events) { const ts = getTags(event).lamport_timestamp; - if (ts && ts > time) { - time = ts; + if (ts) { + this.set(ts); } } - return new LamportTime(time); } now() {