diff --git a/packages/simplex-chat-client/typescript/tests/client.test.ts b/packages/simplex-chat-client/typescript/tests/client.test.ts index 77dca8fa5a..5d2bba75b7 100644 --- a/packages/simplex-chat-client/typescript/tests/client.test.ts +++ b/packages/simplex-chat-client/typescript/tests/client.test.ts @@ -25,7 +25,7 @@ describe.skip("ChatClient (expects SimpleX Chat server with a user, without cont const r2 = await c.msgQ.dequeue() assert.strictEqual(r1.type, "contactConnecting") assert.strictEqual(r2.type, "contactConnected") - const contact1 = (r1 as CEvt.ContactConnected).contact + const contact1 = (r1 as CEvt.ContactConnecting).contact // const contact2 = (r2 as C.CRContactConnected).contact const r3 = await c.apiSendTextMessage(T.ChatType.Direct, contact1.contactId, "hello") assert(r3[0].chatItem.content.type === "sndMsgContent" && r3[0].chatItem.content.msgContent.text === "hello") diff --git a/packages/simplex-chat-nodejs/src/api.ts b/packages/simplex-chat-nodejs/src/api.ts index 6c00287c6f..ef25492fcf 100644 --- a/packages/simplex-chat-nodejs/src/api.ts +++ b/packages/simplex-chat-nodejs/src/api.ts @@ -1,4 +1,4 @@ -import {CC, ChatResponse, T} from "@simplex-chat/types" +import {CC, CEvt, ChatEvent, ChatResponse, T} from "@simplex-chat/types" import * as core from "./core" export class ChatCommandError extends Error { @@ -12,23 +12,121 @@ export enum ConnReqType { Contact = "contact", } +export type EventSubscriberFunc = (event: ChatEvent & {type: K}) => Promise + +interface EventSubscriber { + subscriber: EventSubscriberFunc + once: boolean +} + export class ChatApi { + private receiveEvents = false + private eventsLoop: Promise | undefined = undefined + private subscribers: {[K in CEvt.Tag]?: EventSubscriber[]} = {} + private constructor(protected ctrl_: bigint | undefined) {} - static async init(dbPath: string, dbKey: string, confirm = core.MigrationConfirmation.YesUp): Promise { + static async init( + dbPath: string, + dbKey: string = "", + confirm = core.MigrationConfirmation.YesUp + ): Promise { const ctrl = await core.chatMigrateInit(dbPath, dbKey, confirm) return new ChatApi(ctrl) } + + async startChat(): Promise { + this.receiveEvents = true + this.eventsLoop = this.runEventsLoop() + await this.sendChatCmd("/_start") + // if (r.type !== "chatStarted") throw new ChatCommandError("error starting chat", r) + } + async stopChat(): Promise { + await this.sendChatCmd("/_stop") + // if (r.type !== "chatStopped") throw new ChatCommandError("error starting chat", r) + this.receiveEvents = false + this.eventsLoop = undefined + } + async close(): Promise { + this.receiveEvents = false + this.eventsLoop = undefined await core.chatCloseStore(this.ctrl) this.ctrl_ = undefined } + + private async runEventsLoop(): Promise { + while (this.receiveEvents) { + try { + const event = await this.recvChatEvent() + if (!event) continue + const subs = this.subscribers[event.type] + if (!subs) continue + let i = 0; + while (i < subs.length) { + const {subscriber, once} = subs[i] + try { + await (subscriber as (event: ChatEvent) => Promise)(event) + } catch(e) { + console.log(`${event.type} subsriber error`, e) + } + if (once) subs.splice(i, 1) + else i++ + } + } catch(e) { + console.log("invalid event", e) + } + } + } + on(event: K, subscriber: EventSubscriberFunc, once: boolean = false) { + const subs: EventSubscriber[] = this.subscribers[event] || (this.subscribers[event] = []) + if (!subs.some(s => s.subscriber === subscriber)) { + subs.push({subscriber, once}) + } + } + + once(event: K, subscriber: EventSubscriberFunc) { + this.on(event, subscriber, true) + } + + wait(event: K, predicate: ((event: ChatEvent & {type: K}) => boolean) | undefined = undefined): Promise { + if (predicate) { + return new Promise(resolve => { + const subscriber: EventSubscriberFunc = async (evt: ChatEvent & {type: K}) => { + if (predicate(evt)) { + this.off(event, subscriber) + resolve(evt) + } + } + this.on(event, subscriber) + }) + } else { + return new Promise(resolve => this.once(event, resolve as EventSubscriberFunc)) + } + } + + off(event: K, subscriber: EventSubscriberFunc | undefined = undefined) { + if (subscriber) { + const subs = this.subscribers[event] + if (subs) { + const i = subs.findIndex(s => s.subscriber === subscriber) + if (i !== -1) subs.splice(i, 1) + } + } else { + delete this.subscribers[event] + } + } + get initialized(): boolean { return typeof this.ctrl_ === "bigint" - } - + } + + get receiving(): boolean { + return this.receiveEvents && this.eventsLoop !== undefined + } + get ctrl(): bigint { if (typeof this.ctrl_ === "bigint") return this.ctrl_ else throw Error("chat api controller not initialized") @@ -38,6 +136,10 @@ export class ChatApi { return await core.chatSendCmd(this.ctrl, cmd) } + async recvChatEvent(wait: number = 15_000_000): Promise { + return await core.chatRecvMsgWait(this.ctrl, wait) + } + // Address commands // Bots can use these commands to automatically check and create address when initialized diff --git a/packages/simplex-chat-nodejs/src/core.ts b/packages/simplex-chat-nodejs/src/core.ts index 14ba005f4e..1ab42faa58 100644 --- a/packages/simplex-chat-nodejs/src/core.ts +++ b/packages/simplex-chat-nodejs/src/core.ts @@ -19,6 +19,7 @@ export async function chatCloseStore(ctrl: bigint): Promise { export async function chatSendCmd(ctrl: bigint, cmd: string): Promise { const res = await simplex.chat_send_cmd(ctrl, cmd) const json = JSON.parse(res) as APIResult + // console.log(cmd, json) if (typeof json.result === 'object') return json.result if (typeof json.error === 'object') throw new ChatAPIError("Chat command error (see chatError property)", json.error as T.ChatError) throw new ChatAPIError("Invalid chat command result") @@ -29,6 +30,7 @@ export async function chatRecvMsgWait(ctrl: bigint, wait: number): Promise + // console.log(json) if (typeof json.result === 'object') return json.result if (typeof json.error === 'object') throw new ChatAPIError("Chat event error (see chatError property)", json.error as T.ChatError) throw new ChatAPIError("Invalid chat event") @@ -149,7 +151,7 @@ export namespace MigrationError { export interface MEDowngrade extends Interface { type: "downgrade" - downMigrations: [string] + downMigrations: string[] } export interface MigrationError extends Interface { @@ -181,6 +183,6 @@ export namespace MTRError { export interface MTREDifferent extends Interface { type: "different" - downMigrations: [string] + downMigrations: string[] } } diff --git a/packages/simplex-chat-nodejs/tests/api.test.ts b/packages/simplex-chat-nodejs/tests/api.test.ts new file mode 100644 index 0000000000..804e69c0b3 --- /dev/null +++ b/packages/simplex-chat-nodejs/tests/api.test.ts @@ -0,0 +1,39 @@ +import * as path from "path"; +import * as fs from "fs"; +import {T} from "@simplex-chat/types" +import {api} from "../src/index"; + +describe("Core tests", () => { + const tmpDir = "./tests/tmp2"; + const alicePath = path.join(tmpDir, "alice"); + const bobPath = path.join(tmpDir, "bob"); + + beforeEach(() => fs.mkdirSync(tmpDir, {recursive: true})); + afterEach(() => fs.rmSync(tmpDir, {recursive: true, force: true})); + + it("should send/receive message", async () => { + const a = await api.ChatApi.init(alicePath) + const b = await api.ChatApi.init(bobPath) + const aliceUser = await a.apiCreateActiveUser({displayName: "alice", fullName: ""}) + await b.apiCreateActiveUser({displayName: "bob", fullName: ""}) + await a.startChat() + await b.startChat() + const link = await a.apiCreateLink(aliceUser.userId) + await b.apiConnectActiveUser(link) + const bobContact = (await a.wait("contactConnected")).contact + expect(bobContact).toMatchObject({profile: {displayName: "bob"}}) + const aliceContact = (await b.wait("contactConnected")).contact + expect(aliceContact).toMatchObject({profile: {displayName: "alice"}}) + await a.apiSendTextMessage(T.ChatType.Direct, bobContact.contactId, "hello") + await b.wait("newChatItems", ({chatItems}) => + chatItems.some(({chatItem}) => chatItem.meta.itemText === "hello")) + await b.apiSendTextMessage(T.ChatType.Direct, bobContact.contactId, "hello too") + await a.wait("newChatItems", ({chatItems}) => + chatItems.some(({chatItem}) => chatItem.meta.itemText === "hello too")) + // await a.stopChat() + // await b.stopChat() + await new Promise(resolve => setTimeout(resolve, 2000)) + await a.close() + await b.close() + }, 10000) +}) diff --git a/packages/simplex-chat-nodejs/tests/core.test.ts b/packages/simplex-chat-nodejs/tests/core.test.ts index 501c8ec46e..141f35746d 100644 --- a/packages/simplex-chat-nodejs/tests/core.test.ts +++ b/packages/simplex-chat-nodejs/tests/core.test.ts @@ -6,9 +6,8 @@ describe("Core tests", () => { const tmpDir = "./tests/tmp"; const dbPath = path.join(tmpDir, "simplex_v1"); - beforeEach(() => fs.mkdirSync(tmpDir, { recursive: true })); - - afterEach(() => fs.rmSync(tmpDir, { recursive: true, force: true })); + beforeEach(() => fs.mkdirSync(tmpDir, {recursive: true})); + afterEach(() => fs.rmSync(tmpDir, {recursive: true, force: true})); it("should initialize chat controller", async () => { const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp);