mirror of
https://github.com/BlowaterNostr/blowater.git
synced 2024-10-18 07:33:22 +00:00
fix order issue (#336)
This commit is contained in:
parent
6e19e89fc9
commit
270a427767
13
UI/app.tsx
13
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<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
|
||||
|
@ -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) {
|
||||
|
@ -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, _);
|
||||
{
|
||||
const config2 = await OtherConfig.FromLocalStorage(ctx, _, lamport);
|
||||
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 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"]));
|
||||
}
|
||||
});
|
||||
|
@ -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 event = await prepareEncryptedNostrEvent(this.ctx, {
|
||||
content: JSON.stringify({
|
||||
|
||||
const pin: PinConversationRelay = {
|
||||
pubkey,
|
||||
type: "PinConversation",
|
||||
pubkey: pubkey,
|
||||
}),
|
||||
lamport: this.lamport.now(),
|
||||
};
|
||||
|
||||
const event = await prepareEncryptedNostrEvent(this.ctx, {
|
||||
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 event = await prepareEncryptedNostrEvent(this.ctx, {
|
||||
content: JSON.stringify({
|
||||
|
||||
const unpin: UnpinConversationRelay = {
|
||||
pubkey,
|
||||
type: "UnpinConversation",
|
||||
pubkey: pubkey,
|
||||
}),
|
||||
lamport: this.lamport.now(),
|
||||
};
|
||||
const event = await prepareEncryptedNostrEvent(this.ctx, {
|
||||
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;
|
||||
|
@ -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
|
12
nostr.ts
12
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";
|
||||
};
|
||||
|
10
time.ts
10
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<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() {
|
||||
|
Loading…
Reference in New Issue
Block a user