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 { 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<UI_Interaction_Event>();
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

View File

@ -81,6 +81,7 @@ export async function* UI_Interaction_Update(args: {
pool: ConnectionPool;
popOver: PopOverInputChannel;
newNostrEventChannel: Channel<NostrEvent>;
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) {

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 { 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<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");
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"]));
}
});

View File

@ -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<undefined | Error>;
};
export class OtherConfig implements PinListGetter, NostrEventAdder {
static Empty(nostrEventPusher: Channel<NostrEvent>, ctx: NostrAccountContext) {
return new OtherConfig(nostrEventPusher, ctx);
static Empty(nostrEventPusher: Channel<NostrEvent>, ctx: NostrAccountContext, lamport: LamportTime) {
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()}`);
if (item == null) {
return OtherConfig.Empty(eventPusher, ctx);
return OtherConfig.Empty(eventPusher, ctx, lamport);
}
const event = parseJSON<NostrEvent>(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<NostrEvent>,
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> {
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) {
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<NostrKind.Custom_App_Data>,
event: NostrEvent<NostrKind.Encrypted_Custom_App_Data>,
ctx: NostrAccountContext,
pusher: Channel<NostrEvent>,
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;

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/_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",

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

View File

@ -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";
};

10
time.ts
View File

@ -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<NostrEvent>) {
let time = 0;
fromEvents(events: Iterable<NostrEvent>) {
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() {