fix order issue (#336)

This commit is contained in:
BlowaterNostr 2023-11-25 22:29:50 +08:00 committed by GitHub
parent 6e19e89fc9
commit 270a427767
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 148 additions and 74 deletions

View File

@ -30,10 +30,12 @@ import { ProfileGetter } from "./search.tsx";
import { DirectedMessageController } from "../features/dm.ts"; import { DirectedMessageController } from "../features/dm.ts";
import { ConnectionPool } from "../lib/nostr-ts/relay-pool.ts"; import { ConnectionPool } from "../lib/nostr-ts/relay-pool.ts";
import { LamportTime } from "../time.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) { export async function Start(database: DexieDatabase) {
console.log("Start the application"); console.log("Start the application");
const lamport = new LamportTime();
const model = initialModel(); const model = initialModel();
const eventBus = new EventBus<UI_Interaction_Event>(); const eventBus = new EventBus<UI_Interaction_Event>();
const pool = new ConnectionPool(); const pool = new ConnectionPool();
@ -53,7 +55,7 @@ export async function Start(database: DexieDatabase) {
if (ctx instanceof Error) { if (ctx instanceof Error) {
console.error(ctx); console.error(ctx);
} else if (ctx) { } else if (ctx) {
const otherConfig = await OtherConfig.FromLocalStorage(ctx, newNostrEventChannel); const otherConfig = await OtherConfig.FromLocalStorage(ctx, newNostrEventChannel, lamport);
const app = await App.Start({ const app = await App.Start({
database: dbView, database: dbView,
model, model,
@ -62,6 +64,7 @@ export async function Start(database: DexieDatabase) {
pool, pool,
popOverInputChan, popOverInputChan,
otherConfig, otherConfig,
lamport,
}); });
model.app = app; model.app = app;
} }
@ -84,6 +87,7 @@ export async function Start(database: DexieDatabase) {
pool, pool,
popOver: popOverInputChan, popOver: popOverInputChan,
newNostrEventChannel: newNostrEventChannel, newNostrEventChannel: newNostrEventChannel,
lamport,
}) })
) { ) {
const t = Date.now(); const t = Date.now();
@ -128,8 +132,9 @@ export class App {
pool: ConnectionPool; pool: ConnectionPool;
popOverInputChan: PopOverInputChannel; popOverInputChan: PopOverInputChannel;
otherConfig: OtherConfig; 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); const eventSyncer = new EventSyncer(args.pool, args.database);
// init relay config // init relay config
@ -209,7 +214,7 @@ export class App {
conversationLists, conversationLists,
relayConfig, relayConfig,
groupChatController, groupChatController,
lamport, args.lamport,
dmController, dmController,
); );
await app.initApp(); await app.initApp();
@ -223,7 +228,7 @@ export class App {
(async () => { (async () => {
const stream = await this.pool.newSub(OtherConfig.name, { const stream = await this.pool.newSub(OtherConfig.name, {
authors: [this.ctx.publicKey.hex], authors: [this.ctx.publicKey.hex],
kinds: [NostrKind.Custom_App_Data], kinds: [NostrKind.Encrypted_Custom_App_Data],
}); });
if (stream instanceof Error) { if (stream instanceof Error) {
throw stream; // crash the app throw stream; // crash the app

View File

@ -81,6 +81,7 @@ export async function* UI_Interaction_Update(args: {
pool: ConnectionPool; pool: ConnectionPool;
popOver: PopOverInputChannel; popOver: PopOverInputChannel;
newNostrEventChannel: Channel<NostrEvent>; newNostrEventChannel: Channel<NostrEvent>;
lamport: LamportTime;
}) { }) {
const { model, dbView, eventBus, pool } = args; const { model, dbView, eventBus, pool } = args;
const events = eventBus.onChange(); const events = eventBus.onChange();
@ -91,7 +92,11 @@ export async function* UI_Interaction_Update(args: {
const ctx = event.ctx; const ctx = event.ctx;
if (ctx) { if (ctx) {
console.log("sign in as", ctx.publicKey.bech32()); 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({ const app = await App.Start({
database: dbView, database: dbView,
model, model,
@ -100,6 +105,7 @@ export async function* UI_Interaction_Update(args: {
pool, pool,
popOverInputChan: args.popOver, popOverInputChan: args.popOver,
otherConfig, otherConfig,
lamport: args.lamport,
}); });
model.app = app; model.app = app;
} else { } else {
@ -545,7 +551,7 @@ export async function* Database_Update(
await database.remove(e.id); 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); console.log(e);
const err = await args.otherConfig.addEvent(e); const err = await args.otherConfig.addEvent(e);
if (err instanceof Error) { if (err instanceof Error) {

View File

@ -2,12 +2,14 @@ import { assertEquals, fail } from "https://deno.land/std@0.176.0/testing/assert
import { OtherConfig } from "./config-other.ts"; import { OtherConfig } from "./config-other.ts";
import { InMemoryAccountContext, NostrEvent } from "../lib/nostr-ts/nostr.ts"; import { InMemoryAccountContext, NostrEvent } from "../lib/nostr-ts/nostr.ts";
import { Channel } from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts"; import { Channel } from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts";
import { LamportTime } from "../time.ts";
Deno.test("Pin List", async () => { Deno.test("Pin List", async () => {
const ctx = InMemoryAccountContext.Generate(); const ctx = InMemoryAccountContext.Generate();
const pusher = new Channel<NostrEvent>(); const pusher = new Channel<NostrEvent>();
const _ = new Channel<NostrEvent>(); const _ = new Channel<NostrEvent>();
const config = OtherConfig.Empty(pusher, ctx); const lamport = new LamportTime();
const config = OtherConfig.Empty(pusher, ctx, lamport);
await config.addPin("a"); await config.addPin("a");
assertEquals(config.getPinList(), new Set(["a"])); assertEquals(config.getPinList(), new Set(["a"]));
@ -16,34 +18,47 @@ Deno.test("Pin List", async () => {
assertEquals(config.getPinList(), new Set(["a", "b"])); assertEquals(config.getPinList(), new Set(["a", "b"]));
// able to restore the config from local storage // able to restore the config from local storage
const config2 = await OtherConfig.FromLocalStorage(ctx, _); {
const config2 = await OtherConfig.FromLocalStorage(ctx, _, lamport);
assertEquals(config2.getPinList(), new Set(["a", "b"])); assertEquals(config2.getPinList(), new Set(["a", "b"]));
assertEquals(config2.getPinList(), config.getPinList()); 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 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 // remove 1 pin from config1
await config.removePin("a"); await config.removePin("a");
assertEquals(config.getPinList(), new Set(["b"])); assertEquals(config.getPinList(), new Set(["b"]));
// config3 is able to sync with config1 // 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); const err = await config3.addEvent(event3);
if (err instanceof Error) fail(err.message); 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"])); 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"]));
}
}); });

View File

@ -3,77 +3,102 @@ import { NostrAccountContext, NostrEvent, NostrKind, verifyEvent } from "../lib/
import { PinListGetter } from "./conversation-list.tsx"; import { PinListGetter } from "./conversation-list.tsx";
import { parseJSON } from "../features/profile.ts"; import { parseJSON } from "../features/profile.ts";
import { Channel } from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.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 = { export type NostrEventAdder = {
addEvent(event: NostrEvent): Promise<undefined | Error>; addEvent(event: NostrEvent): Promise<undefined | Error>;
}; };
export class OtherConfig implements PinListGetter, NostrEventAdder { export class OtherConfig implements PinListGetter, NostrEventAdder {
static Empty(nostrEventPusher: Channel<NostrEvent>, ctx: NostrAccountContext) { static Empty(nostrEventPusher: Channel<NostrEvent>, ctx: NostrAccountContext, lamport: LamportTime) {
return new OtherConfig(nostrEventPusher, ctx); return new OtherConfig(nostrEventPusher, ctx, lamport);
} }
static async FromLocalStorage(ctx: NostrAccountContext, eventPusher: Channel<NostrEvent>) { static async FromLocalStorage(
ctx: NostrAccountContext,
eventPusher: Channel<NostrEvent>,
lamport: LamportTime,
) {
const item = localStorage.getItem(`${OtherConfig.name}:${ctx.publicKey.bech32()}`); const item = localStorage.getItem(`${OtherConfig.name}:${ctx.publicKey.bech32()}`);
if (item == null) { if (item == null) {
return OtherConfig.Empty(eventPusher, ctx); return OtherConfig.Empty(eventPusher, ctx, lamport);
} }
const event = parseJSON<NostrEvent>(item); const event = parseJSON<NostrEvent>(item);
if (event instanceof Error) { if (event instanceof Error) {
console.error(event); console.error(event);
return OtherConfig.Empty(eventPusher, ctx); return OtherConfig.Empty(eventPusher, ctx, lamport);
} }
const ok = await verifyEvent(event); const ok = await verifyEvent(event);
if (!ok) { 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( const config = await OtherConfig.FromNostrEvent(
// @ts-ignore {
event, ...event,
kind: event.kind,
},
ctx, ctx,
eventPusher, eventPusher,
lamport,
); );
if (config instanceof Error) { if (config instanceof Error) {
return OtherConfig.Empty(eventPusher, ctx); return OtherConfig.Empty(eventPusher, ctx, lamport);
} }
return config; return config;
} }
return OtherConfig.Empty(eventPusher, ctx); return OtherConfig.Empty(eventPusher, ctx, lamport);
} }
private constructor( private constructor(
private readonly nostrEventPusher: Channel<NostrEvent>, private readonly nostrEventPusher: Channel<NostrEvent>,
private readonly ctx: NostrAccountContext, private readonly ctx: NostrAccountContext,
private readonly lamport: LamportTime,
) {} ) {}
private pinList = new Set<string>(); // set of pubkeys in npub format private pinList = new Map<string, PinConversationRelay | UnpinConversationRelay>(); // set of pubkeys in npub format
getPinList(): Set<string> { getPinList(): Set<string> {
return this.pinList; const set = new Set<string>();
for (const event of this.pinList.values()) {
if (event.type == "PinConversation") {
set.add(event.pubkey);
}
}
return set;
} }
async addPin(pubkey: string) { async addPin(pubkey: string) {
if (this.pinList.has(pubkey)) { const currentPin = this.pinList.get(pubkey);
if (currentPin && currentPin.type == "PinConversation") {
return; return;
} }
this.pinList.add(pubkey);
const err = await this.saveToLocalStorage(); const pin: PinConversationRelay = {
if (err instanceof Error) { pubkey,
return err;
}
const event = await prepareEncryptedNostrEvent(this.ctx, {
content: JSON.stringify({
type: "PinConversation", type: "PinConversation",
pubkey: pubkey, lamport: this.lamport.now(),
}), };
const event = await prepareEncryptedNostrEvent(this.ctx, {
content: JSON.stringify(pin),
encryptKey: this.ctx.publicKey, encryptKey: this.ctx.publicKey,
kind: NostrKind.Custom_App_Data, kind: NostrKind.Encrypted_Custom_App_Data,
}); });
if (event instanceof Error) { if (event instanceof Error) {
return event; return event;
} }
this.pinList.set(pubkey, pin);
const err = await this.saveToLocalStorage();
if (err instanceof Error) {
return err;
}
/* no await */ this.nostrEventPusher.put(event); /* no await */ this.nostrEventPusher.put(event);
} }
@ -82,24 +107,33 @@ export class OtherConfig implements PinListGetter, NostrEventAdder {
if (!exist) { if (!exist) {
return; return;
} }
const event = await prepareEncryptedNostrEvent(this.ctx, {
content: JSON.stringify({ const unpin: UnpinConversationRelay = {
pubkey,
type: "UnpinConversation", type: "UnpinConversation",
pubkey: pubkey, lamport: this.lamport.now(),
}), };
const event = await prepareEncryptedNostrEvent(this.ctx, {
content: JSON.stringify(unpin),
encryptKey: this.ctx.publicKey, encryptKey: this.ctx.publicKey,
kind: NostrKind.Custom_App_Data, kind: NostrKind.Encrypted_Custom_App_Data,
}); });
if (event instanceof Error) { if (event instanceof Error) {
return event; return event;
} }
this.pinList.set(pubkey, unpin);
const err = await this.saveToLocalStorage();
if (err instanceof Error) {
return err;
}
/* no await */ this.nostrEventPusher.put(event); /* no await */ this.nostrEventPusher.put(event);
} }
static async FromNostrEvent( static async FromNostrEvent(
event: NostrEvent<NostrKind.Custom_App_Data>, event: NostrEvent<NostrKind.Encrypted_Custom_App_Data>,
ctx: NostrAccountContext, ctx: NostrAccountContext,
pusher: Channel<NostrEvent>, pusher: Channel<NostrEvent>,
lamport: LamportTime,
) { ) {
const decrypted = await ctx.decrypt(ctx.publicKey.hex, event.content); const decrypted = await ctx.decrypt(ctx.publicKey.hex, event.content);
if (decrypted instanceof Error) { if (decrypted instanceof Error) {
@ -118,7 +152,7 @@ export class OtherConfig implements PinListGetter, NostrEventAdder {
pinList = []; pinList = [];
} }
const c = new OtherConfig(pusher, ctx); const c = new OtherConfig(pusher, ctx, lamport);
for (const pin of pinList) { for (const pin of pinList) {
const err = await c.addPin(pin); const err = await c.addPin(pin);
if (err instanceof Error) { if (err instanceof Error) {
@ -131,8 +165,8 @@ export class OtherConfig implements PinListGetter, NostrEventAdder {
private async toNostrEvent(ctx: NostrAccountContext) { private async toNostrEvent(ctx: NostrAccountContext) {
const event = await prepareEncryptedNostrEvent(ctx, { const event = await prepareEncryptedNostrEvent(ctx, {
encryptKey: ctx.publicKey, encryptKey: ctx.publicKey,
content: JSON.stringify(Array.from(this.pinList)), content: JSON.stringify(Array.from(this.getPinList())),
kind: NostrKind.Custom_App_Data, kind: NostrKind.Encrypted_Custom_App_Data,
tags: [], tags: [],
}); });
return event; return event;
@ -147,7 +181,7 @@ export class OtherConfig implements PinListGetter, NostrEventAdder {
} }
async addEvent(event: NostrEvent) { async addEvent(event: NostrEvent) {
if (event.kind != NostrKind.Custom_App_Data) { if (event.kind != NostrKind.Encrypted_Custom_App_Data) {
return; return;
} }
const decrypted = await this.ctx.decrypt(this.ctx.publicKey.hex, event.content); 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" || pin.type == "UnpinConversation") {
if (pin.type == "PinConversation") { const currentEvent = this.pinList.get(pin.pubkey);
if (this.pinList.has(pin.pubkey)) {
return; if (currentEvent && pin.lamport < currentEvent.lamport) {
} return; // ignore because the current event is newer
this.pinList.add(pin.pubkey);
} else {
this.pinList.delete(pin.pubkey);
} }
this.lamport.set(pin.lamport);
this.pinList.set(pin.pubkey, pin);
const err = await this.saveToLocalStorage(); const err = await this.saveToLocalStorage();
if (err instanceof Error) { if (err instanceof Error) {
return err; return err;
@ -176,4 +211,4 @@ export class OtherConfig implements PinListGetter, NostrEventAdder {
} }
} }
export type ConfigEvent = PinConversation | UnpinConversation; export type ConfigEvent = PinConversationRelay | UnpinConversationRelay;

View File

@ -6,6 +6,9 @@
"https://deno.land/std@0.176.0/testing/_diff.ts": "1a3c044aedf77647d6cac86b798c6417603361b66b54c53331b312caeb447aea", "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/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7",
"https://deno.land/std@0.176.0/testing/asserts.ts": "984ab0bfb3faeed92ffaa3a6b06536c66811185328c5dd146257c702c41b01ab", "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/@noble/hashes@1.3.2/utils": "20c519683900b5873b16ff15377049f6e86e183b612a0e442f6acbc056667e6a",
"https://esm.sh/@scure/bip32@1.3.2": "8f8111ae2b0865644daf69d6b0d8ea76bb15112453f5dc697ba29b44b527b26c", "https://esm.sh/@scure/bip32@1.3.2": "8f8111ae2b0865644daf69d6b0d8ea76bb15112453f5dc697ba29b44b527b26c",
"https://esm.sh/@scure/bip39@1.2.1": "7d6cdfce191281c81406e55de5216714dd22b19790123b652421bbf9718e5057", "https://esm.sh/@scure/bip39@1.2.1": "7d6cdfce191281c81406e55de5216714dd22b19790123b652421bbf9718e5057",

@ -1 +1 @@
Subproject commit 8afbf3999af91d9f7320789509abbfdb1405d17c Subproject commit be759ead56a6eb8984e024296f79506b42c89199

View File

@ -58,11 +58,23 @@ export type PinConversation = {
pubkey: string; pubkey: string;
}; };
export type PinConversationRelay = {
type: "PinConversation";
pubkey: string;
lamport: number;
};
export type UnpinConversation = { export type UnpinConversation = {
type: "UnpinConversation"; type: "UnpinConversation";
pubkey: string; pubkey: string;
}; };
export type UnpinConversationRelay = {
type: "UnpinConversation";
pubkey: string;
lamport: number;
};
export type UserLogin = { export type UserLogin = {
type: "UserLogin"; type: "UserLogin";
}; };

10
time.ts
View File

@ -2,17 +2,15 @@ import { NostrEvent } from "./lib/nostr-ts/nostr.ts";
import { getTags } from "./nostr.ts"; import { getTags } from "./nostr.ts";
export class LamportTime { export class LamportTime {
constructor(private time: number) {} private time = 0;
static FromEvents(events: Iterable<NostrEvent>) { fromEvents(events: Iterable<NostrEvent>) {
let time = 0;
for (const event of events) { for (const event of events) {
const ts = getTags(event).lamport_timestamp; const ts = getTags(event).lamport_timestamp;
if (ts && ts > time) { if (ts) {
time = ts; this.set(ts);
} }
} }
return new LamportTime(time);
} }
now() { now() {