From 2faf72f6e13062006e999e575fda6c36f9b4c764 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 10 Jan 2026 22:27:09 +0000 Subject: [PATCH] simplify cpp add-on - move logic to JS, fix API --- packages/simplex-chat-nodejs/cpp/simplex.cc | 196 ++++-------------- packages/simplex-chat-nodejs/src/api.ts | 3 +- packages/simplex-chat-nodejs/src/core.d.ts | 26 --- packages/simplex-chat-nodejs/src/core.ts | 186 +++++++++++++++++ packages/simplex-chat-nodejs/src/simplex.d.ts | 12 ++ .../src/{core.js => simplex.js} | 0 packages/simplex-chat-nodejs/src/types.ts | 101 --------- .../simplex-chat-nodejs/tests/core.test.ts | 68 +++--- 8 files changed, 276 insertions(+), 316 deletions(-) delete mode 100644 packages/simplex-chat-nodejs/src/core.d.ts create mode 100644 packages/simplex-chat-nodejs/src/core.ts create mode 100644 packages/simplex-chat-nodejs/src/simplex.d.ts rename packages/simplex-chat-nodejs/src/{core.js => simplex.js} (100%) delete mode 100644 packages/simplex-chat-nodejs/src/types.ts diff --git a/packages/simplex-chat-nodejs/cpp/simplex.cc b/packages/simplex-chat-nodejs/cpp/simplex.cc index 07daded2ff..c865449e7e 100644 --- a/packages/simplex-chat-nodejs/cpp/simplex.cc +++ b/packages/simplex-chat-nodejs/cpp/simplex.cc @@ -22,19 +22,12 @@ void haskell_init() { hs_init_with_rtsopts(&argc, &pargv); } -Napi::Value ParseJson(Env env, const std::string& json_str) { - Object global = env.Global(); - Object json = global.Get("JSON").As(); - Function parse = json.Get("parse").As(); - return parse.Call(json, {String::New(env, json_str)}); -} - -class JsonAsyncWorker : public AsyncWorker { +class ResultAsyncWorker : public AsyncWorker { public: - using ExecuteFn = std::function; - using ResultProcessor = std::function; + using ExecuteFn = std::function; + using ResultProcessor = std::function; - JsonAsyncWorker(Function& callback, ExecuteFn execute_fn, ResultProcessor result_processor = nullptr) + ResultAsyncWorker(Function& callback, ExecuteFn execute_fn, ResultProcessor result_processor = nullptr) : AsyncWorker(callback), execute_fn_(std::move(execute_fn)), result_processor_(std::move(result_processor)) {} void Execute() override { @@ -67,21 +60,6 @@ class JsonAsyncWorker : public AsyncWorker { return result_; } - Value ParseAndHandleJson(Napi::Env env, bool allow_empty = false) { - if (result_.empty() && allow_empty) return env.Undefined(); - if (result_.empty()) { - Callback().Call({Error::New(env, "Empty result").Value(), env.Undefined()}); - return env.Undefined(); - } - Value parsed = ParseJson(env, result_); - if (env.IsExceptionPending()) { - Error exception = env.GetAndClearPendingException(); - Callback().Call({exception.Value(), env.Undefined()}); - return env.Undefined(); - } - return parsed; - } - void SetCtrl(uintptr_t ctrl) { ctrl_ = ctrl; } @@ -162,18 +140,14 @@ chat_ctrl FromChatCtrlBigInt(const Napi::Value& value) { return reinterpret_cast(val); } -// Helper for handling common C result patterns (where empty res is error) -void HandleCResult(JsonAsyncWorker* worker, char* c_res, const std::string& func_name) { +// Helper for handling common C result patterns (no empty check) +void HandleCResult(ResultAsyncWorker* worker, char* c_res, const std::string& func_name) { if (c_res == nullptr) { worker->SetWorkerError(func_name + " failed"); return; } std::string res = c_res; free(c_res); - if (res.empty()) { - worker->SetWorkerError(func_name + " failed"); - return; - } worker->SetResult(res); } @@ -190,75 +164,12 @@ Napi::Promise CreatePromiseAndCallback(Env env, Function& cb_out) { } // Common result processors -JsonAsyncWorker::ResultProcessor JsonResultProcessor() { - return [](JsonAsyncWorker* worker, Napi::Env env) { - Value parsed = worker->ParseAndHandleJson(env); - if (parsed.IsUndefined()) return; - worker->Callback().Call({env.Null(), parsed}); - }; -} - -JsonAsyncWorker::ResultProcessor RecvResultProcessor() { - return [](JsonAsyncWorker* worker, Napi::Env env) { - Value parsed = worker->ParseAndHandleJson(env, true); // Allow empty - if (parsed.IsUndefined() && !worker->GetStringResult().empty()) return; - worker->Callback().Call({env.Null(), parsed}); - }; -} - -JsonAsyncWorker::ResultProcessor WriteResultProcessor() { - return [](JsonAsyncWorker* worker, Napi::Env env) { - Value parsed = worker->ParseAndHandleJson(env); - if (parsed.IsUndefined()) return; - Object parsed_obj = parsed.As(); - Value type_val = parsed_obj.Get("type"); - if (!type_val.IsString()) { - Error err = Error::New(env, "Invalid response type"); - worker->Callback().Call({err.Value(), env.Undefined()}); - return; - } - std::string type = type_val.As().Utf8Value(); - if (type == "error") { - Value err_val = parsed_obj.Get("writeError"); - std::string err_msg = err_val.IsString() ? err_val.As().Utf8Value() : "Unknown error"; - Error err = Error::New(env, err_msg); - worker->Callback().Call({err.Value(), env.Undefined()}); - } else { - Value cryptoArgs = parsed_obj.Get("cryptoArgs"); - if (cryptoArgs.IsUndefined()) { - Error err = Error::New(env, "Missing cryptoArgs"); - worker->Callback().Call({err.Value(), env.Undefined()}); - return; - } - worker->Callback().Call({env.Null(), cryptoArgs}); - } - }; -} - -JsonAsyncWorker::ResultProcessor MigrateResultProcessor() { - return [](JsonAsyncWorker* worker, Napi::Env env) { - Value parsed = worker->ParseAndHandleJson(env); - if (parsed.IsUndefined()) return; - Object parsed_obj = parsed.As(); - Value type_val = parsed_obj.Get("type"); - if (type_val.IsString() && type_val.As().Utf8Value() == "ok") { - worker->Callback().Call({env.Null(), ToChatCtrlBigInt(env, worker->GetCtrl())}); - } else { - Error err = Error::New(env, "Database or migration error (see dbMigrationError property)"); - err.Set("dbMigrationError", parsed_obj); - worker->Callback().Call({err.Value(), env.Undefined()}); - } - }; -} - -JsonAsyncWorker::ResultProcessor ErrorResultProcessor() { - return [](JsonAsyncWorker* worker, Napi::Env env) { - if (worker->GetStringResult().empty()) { - worker->Callback().Call({env.Null(), env.Undefined()}); - } else { - Error err = Error::New(env, worker->GetStringResult()); - worker->Callback().Call({err.Value(), env.Undefined()}); - } +ResultAsyncWorker::ResultProcessor MigrateResultProcessor() { + return [](ResultAsyncWorker* worker, Napi::Env env) { + Napi::Array arr = Napi::Array::New(env, 2); + arr.Set(0u, ToChatCtrlBigInt(env, worker->GetCtrl())); + arr.Set(1u, Napi::String::New(env, worker->GetStringResult())); + worker->Callback().Call({env.Null(), arr}); }; } @@ -278,20 +189,14 @@ Value ChatMigrateInit(const CallbackInfo& args) { Function cb; Promise promise = CreatePromiseAndCallback(env, cb); - auto execute_fn = [path, key, confirm](JsonAsyncWorker* worker) { + auto execute_fn = [path, key, confirm](ResultAsyncWorker* worker) { chat_ctrl ctrl = nullptr; char* c_res = chat_migrate_init(path.c_str(), key.c_str(), confirm.c_str(), &ctrl); - if (c_res == nullptr) { - worker->SetWorkerError("chat_migrate_init failed"); - return; - } - std::string res = c_res; - free(c_res); worker->SetCtrl(reinterpret_cast(ctrl)); - worker->SetResult(res); + HandleCResult(worker, c_res, "chat_migrate_init"); }; - JsonAsyncWorker* worker = new JsonAsyncWorker(cb, std::move(execute_fn), MigrateResultProcessor()); + ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn), MigrateResultProcessor()); worker->Queue(); return promise; @@ -309,18 +214,12 @@ Value ChatCloseStore(const CallbackInfo& args) { Function cb; Promise promise = CreatePromiseAndCallback(env, cb); - auto execute_fn = [ctrl](JsonAsyncWorker* worker) { + auto execute_fn = [ctrl](ResultAsyncWorker* worker) { char* c_res = chat_close_store(ctrl); - if (c_res == nullptr) { - worker->SetWorkerError("chat_close_store failed"); - return; - } - std::string res = c_res; - free(c_res); - worker->SetResult(res); + HandleCResult(worker, c_res, "chat_close_store"); }; - JsonAsyncWorker* worker = new JsonAsyncWorker(cb, std::move(execute_fn), ErrorResultProcessor()); + ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn)); worker->Queue(); return promise; @@ -339,12 +238,12 @@ Value ChatSendCmd(const CallbackInfo& args) { Function cb; Promise promise = CreatePromiseAndCallback(env, cb); - auto execute_fn = [ctrl, cmd](JsonAsyncWorker* worker) { + auto execute_fn = [ctrl, cmd](ResultAsyncWorker* worker) { char* c_res = chat_send_cmd(ctrl, cmd.c_str()); HandleCResult(worker, c_res, "chat_send_cmd"); }; - JsonAsyncWorker* worker = new JsonAsyncWorker(cb, std::move(execute_fn), JsonResultProcessor()); + ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn)); worker->Queue(); return promise; @@ -363,18 +262,12 @@ Value ChatRecvMsgWait(const CallbackInfo& args) { Function cb; Promise promise = CreatePromiseAndCallback(env, cb); - auto execute_fn = [ctrl, wait](JsonAsyncWorker* worker) { + auto execute_fn = [ctrl, wait](ResultAsyncWorker* worker) { char* c_res = chat_recv_msg_wait(ctrl, wait); - if (c_res == nullptr) { - worker->SetWorkerError("chat_recv_msg_wait failed"); - return; - } - std::string res = c_res; - free(c_res); - worker->SetResult(res); + HandleCResult(worker, c_res, "chat_recv_msg_wait"); }; - JsonAsyncWorker* worker = new JsonAsyncWorker(cb, std::move(execute_fn), RecvResultProcessor()); + ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn)); worker->Queue(); return promise; @@ -391,19 +284,18 @@ Value ChatWriteFile(const CallbackInfo& args) { std::string path = args[1].As().Utf8Value(); ArrayBuffer ab = args[2].As(); char* data = static_cast(ab.Data()); - int len = static_cast(ab.ByteLength()); + size_t len = ab.ByteLength(); Function cb; Promise promise = CreatePromiseAndCallback(env, cb); - auto execute_fn = [ctrl, path, data, len](JsonAsyncWorker* worker) { - char* c_res = chat_write_file(ctrl, path.c_str(), data, len); + auto execute_fn = [ctrl, path, ab, data, len](ResultAsyncWorker* worker) { + (void)ab; // to keep ArrayBuffer alive + char* c_res = chat_write_file(ctrl, path.c_str(), data, static_cast(len)); HandleCResult(worker, c_res, "chat_write_file"); }; - // Note: To keep ab alive, we can use a Reference, but since data is used in lambda capture by value, it's fine as long as the worker lives. - // If needed, add Reference to JsonAsyncWorker for this case. - JsonAsyncWorker* worker = new JsonAsyncWorker(cb, std::move(execute_fn), WriteResultProcessor()); + ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn)); worker->Queue(); return promise; @@ -466,12 +358,12 @@ Value ChatEncryptFile(const CallbackInfo& args) { Function cb; Promise promise = CreatePromiseAndCallback(env, cb); - auto execute_fn = [ctrl, fromPath, toPath](JsonAsyncWorker* worker) { + auto execute_fn = [ctrl, fromPath, toPath](ResultAsyncWorker* worker) { char* c_res = chat_encrypt_file(ctrl, fromPath.c_str(), toPath.c_str()); HandleCResult(worker, c_res, "chat_encrypt_file"); }; - JsonAsyncWorker* worker = new JsonAsyncWorker(cb, std::move(execute_fn), WriteResultProcessor()); + ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn)); worker->Queue(); return promise; @@ -492,18 +384,12 @@ Value ChatDecryptFile(const CallbackInfo& args) { Function cb; Promise promise = CreatePromiseAndCallback(env, cb); - auto execute_fn = [fromPath, key, nonce, toPath](JsonAsyncWorker* worker) { + auto execute_fn = [fromPath, key, nonce, toPath](ResultAsyncWorker* worker) { char* c_res = chat_decrypt_file(fromPath.c_str(), key.c_str(), nonce.c_str(), toPath.c_str()); - if (c_res == nullptr) { - worker->SetWorkerError("chat_decrypt_file failed"); - return; - } - std::string res = c_res; - free(c_res); - worker->SetResult(res); + HandleCResult(worker, c_res, "chat_decrypt_file"); }; - JsonAsyncWorker* worker = new JsonAsyncWorker(cb, std::move(execute_fn), ErrorResultProcessor()); + ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn)); worker->Queue(); return promise; @@ -511,14 +397,14 @@ Value ChatDecryptFile(const CallbackInfo& args) { Object Init(Env env, Object exports) { haskell_init(); - exports.Set("chatMigrateInit", Function::New(env, ChatMigrateInit)); - exports.Set("chatCloseStore", Function::New(env, ChatCloseStore)); - exports.Set("chatSendCmd", Function::New(env, ChatSendCmd)); - exports.Set("chatRecvMsgWait", Function::New(env, ChatRecvMsgWait)); - exports.Set("chatWriteFile", Function::New(env, ChatWriteFile)); - exports.Set("chatReadFile", Function::New(env, ChatReadFile)); - exports.Set("chatEncryptFile", Function::New(env, ChatEncryptFile)); - exports.Set("chatDecryptFile", Function::New(env, ChatDecryptFile)); + exports.Set("chat_migrate_init", Function::New(env, ChatMigrateInit)); + exports.Set("chat_close_store", Function::New(env, ChatCloseStore)); + exports.Set("chat_send_cmd", Function::New(env, ChatSendCmd)); + exports.Set("chat_recv_msg_wait", Function::New(env, ChatRecvMsgWait)); + exports.Set("chat_write_file", Function::New(env, ChatWriteFile)); + exports.Set("chat_read_file", Function::New(env, ChatReadFile)); + exports.Set("chat_encrypt_file", Function::New(env, ChatEncryptFile)); + exports.Set("chat_decrypt_file", Function::New(env, ChatDecryptFile)); return exports; } diff --git a/packages/simplex-chat-nodejs/src/api.ts b/packages/simplex-chat-nodejs/src/api.ts index 8b8ff97a9c..6c00287c6f 100644 --- a/packages/simplex-chat-nodejs/src/api.ts +++ b/packages/simplex-chat-nodejs/src/api.ts @@ -1,6 +1,5 @@ import {CC, ChatResponse, T} from "@simplex-chat/types" import * as core from "./core" -import {MigrationConfirmation} from "./types" export class ChatCommandError extends Error { constructor(public message: string, public response: ChatResponse) { @@ -16,7 +15,7 @@ export enum ConnReqType { export class ChatApi { private constructor(protected ctrl_: bigint | undefined) {} - static async init(dbPath: string, dbKey: string, confirm = 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) } diff --git a/packages/simplex-chat-nodejs/src/core.d.ts b/packages/simplex-chat-nodejs/src/core.d.ts deleted file mode 100644 index 9e4eb2fd6d..0000000000 --- a/packages/simplex-chat-nodejs/src/core.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {ChatEvent, ChatResponse} from "@simplex-chat/types" -import {CryptoArgs, MigrationConfirmation} from "./types" - -// initialize chat controller -export function chatMigrateInit(dbPath: string, dbKey: string, confirm: MigrationConfirmation): Promise - -// close chat store -export function chatCloseStore(ctrl: bigint): Promise - -// send chat command as string -export function chatSendCmd(ctrl: bigint, cmd: string): Promise - -// receive chat event -export function chatRecvMsgWait(ctrl: bigint, wait: number): Promise - -// write buffer to encrypted file -export function chatWriteFile(ctrl: bigint, path: string, buffer: ArrayBuffer): Promise - -// read buffer from encrypted file -export function chatReadFile(path: string, key: string, nonce: string): Promise - -// encrypt file -export function chatEncryptFile(ctrl: bigint, fromPath: string, toPath: string): Promise - -// decrypt file -export function chatDecryptFile(fromPath: string, key: string, nonce: string, toPath: string): Promise diff --git a/packages/simplex-chat-nodejs/src/core.ts b/packages/simplex-chat-nodejs/src/core.ts new file mode 100644 index 0000000000..14ba005f4e --- /dev/null +++ b/packages/simplex-chat-nodejs/src/core.ts @@ -0,0 +1,186 @@ +import {ChatEvent, ChatResponse, T} from "@simplex-chat/types" +import * as simplex from "./simplex" + +// initialize chat controller +export async function chatMigrateInit(dbPath: string, dbKey: string, confirm: MigrationConfirmation): Promise { + const [ctrl, res] = await simplex.chat_migrate_init(dbPath, dbKey, confirm) + const json = JSON.parse(res) + if (json.type === 'ok') return ctrl + throw new ChatInitError("Database or migration error (see dbMigrationError property)", json as DBMigrationError) +} + +// close chat store +export async function chatCloseStore(ctrl: bigint): Promise { + const res = await simplex.chat_close_store(ctrl) + if (res !== "") throw new Error(res) +} + +// send chat command as string +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 + 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") +} + +// receive chat event +export async function chatRecvMsgWait(ctrl: bigint, wait: number): Promise { + const res = await simplex.chat_recv_msg_wait(ctrl, wait) + if (res === "") return undefined + const json = JSON.parse(res) as APIResult + 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") +} + +// write buffer to encrypted file +export async function chatWriteFile(ctrl: bigint, path: string, buffer: ArrayBuffer): Promise { + const res = await simplex.chat_write_file(ctrl, path, buffer) + return cryptoArgsResult(res) +} + +// read buffer from encrypted file +export async function chatReadFile(path: string, {fileKey, fileNonce}: CryptoArgs): Promise { + return await simplex.chat_read_file(path, fileKey, fileNonce) +} + +// encrypt file +export async function chatEncryptFile(ctrl: bigint, fromPath: string, toPath: string): Promise { + const res = await simplex.chat_encrypt_file(ctrl, fromPath, toPath) + return cryptoArgsResult(res) +} + +// decrypt file +export async function chatDecryptFile(fromPath: string, {fileKey, fileNonce}: CryptoArgs, toPath: string): Promise { + const res = await simplex.chat_decrypt_file(fromPath, fileKey, fileNonce, toPath) + if (res !== "") throw new Error(res) +} + +function cryptoArgsResult(res: string): CryptoArgs { + const json = JSON.parse(res) + switch (json.type) { + case "result": return json.cryptoArgs as CryptoArgs + case "error": throw Error(json.writeError) + default: throw Error("unexpected chat_write_file result: " + res) + } +} + +export interface APIResult { + result?: R + error?: T.ChatError +} + +export class ChatAPIError extends Error { + constructor(public message: string, public chatError: T.ChatError | undefined = undefined) { + super(message) + } +} + +export enum MigrationConfirmation { + YesUp = "yesUp", + YesUpDown = "yesUpDown", + Console = "console", + Error = "error" +} + +export interface CryptoArgs { + fileKey: string + fileNonce: string +} + +export class ChatInitError extends Error { + constructor(public message: string, public dbMigrationError: DBMigrationError) { + super(message) + } +} + +export type DBMigrationError = + | DBMigrationError.InvalidConfirmation + | DBMigrationError.ErrorNotADatabase // invalid/corrupt database file or incorrect encryption key + | DBMigrationError.ErrorMigration + | DBMigrationError.ErrorSQL + +export namespace DBMigrationError { + export type Tag = "invalidConfirmation" | "errorNotADatabase" | "errorMigration" | "errorSQL" + + interface Interface { + type: Tag + } + + export interface InvalidConfirmation extends Interface { + type: "invalidConfirmation" + } + + export interface ErrorNotADatabase extends Interface { + type: "errorNotADatabase" + dbFile: string + } + + export interface ErrorMigration extends Interface { + type: "errorMigration" + dbFile: string + migrationError: MigrationError + } + + export interface ErrorSQL extends Interface { + type: "errorSQL" + dbFile: string + migrationSQLError: string + } +} + +export type MigrationError = + | MigrationError.MEUpgrade + | MigrationError.MEDowngrade + | MigrationError.MigrationError + +export namespace MigrationError { + export type Tag = "upgrade" | "downgrade" | "migrationError" + + interface Interface { + type: Tag + } + + export interface MEUpgrade extends Interface { + type: "upgrade" + upMigrations: UpMigration + } + + export interface MEDowngrade extends Interface { + type: "downgrade" + downMigrations: [string] + } + + export interface MigrationError extends Interface { + type: "migrationError" + mtrError: MTRError + } +} + +export interface UpMigration { + upName: string + withDown: boolean +} + +export type MTRError = + | MTRError.MTRENoDown + | MTRError.MTREDifferent + +export namespace MTRError { + export type Tag = "noDown" | "different" + + interface Interface { + type: Tag + } + + export interface MTRENoDown extends Interface { + type: "noDown" + upMigrations: UpMigration + } + + export interface MTREDifferent extends Interface { + type: "different" + downMigrations: [string] + } +} diff --git a/packages/simplex-chat-nodejs/src/simplex.d.ts b/packages/simplex-chat-nodejs/src/simplex.d.ts new file mode 100644 index 0000000000..8b93161a3c --- /dev/null +++ b/packages/simplex-chat-nodejs/src/simplex.d.ts @@ -0,0 +1,12 @@ +import {ChatEvent, ChatResponse} from "@simplex-chat/types"; + +// These functions are defined in CPP add-on ../cpp/simplex.cc + +export function chat_migrate_init(dbPath: string, dbKey: string, confirm: string): Promise<[bigint, string]> +export function chat_close_store(ctrl: bigint): Promise +export function chat_send_cmd(ctrl: bigint, cmd: string): Promise +export function chat_recv_msg_wait(ctrl: bigint, wait: number): Promise +export function chat_write_file(ctrl: bigint, path: string, buffer: ArrayBuffer): Promise +export function chat_read_file(path: string, key: string, nonce: string): Promise +export function chat_encrypt_file(ctrl: bigint, fromPath: string, toPath: string): Promise +export function chat_decrypt_file(fromPath: string, key: string, nonce: string, toPath: string): Promise diff --git a/packages/simplex-chat-nodejs/src/core.js b/packages/simplex-chat-nodejs/src/simplex.js similarity index 100% rename from packages/simplex-chat-nodejs/src/core.js rename to packages/simplex-chat-nodejs/src/simplex.js diff --git a/packages/simplex-chat-nodejs/src/types.ts b/packages/simplex-chat-nodejs/src/types.ts deleted file mode 100644 index 233cabc684..0000000000 --- a/packages/simplex-chat-nodejs/src/types.ts +++ /dev/null @@ -1,101 +0,0 @@ -export enum MigrationConfirmation { - YesUp = "yesUp", - YesUpDown = "yesUpDown", - Console = "console", - Error = "error" -} - -export interface CryptoArgs { - fileKey: string - fileNonce: string -} - -export type DBMigrationError = - | DBMigrationError.InvalidConfirmation - | DBMigrationError.ErrorNotADatabase // invalid/corrupt database file or incorrect encryption key - | DBMigrationError.ErrorMigration - | DBMigrationError.ErrorSQL - -export namespace DBMigrationError { - export type Tag = "invalidConfirmation" | "errorNotADatabase" | "errorMigration" | "errorSQL" - - interface Interface { - type: Tag - } - - export interface InvalidConfirmation extends Interface { - type: "invalidConfirmation" - } - - export interface ErrorNotADatabase extends Interface { - type: "errorNotADatabase" - dbFile: string - } - - export interface ErrorMigration extends Interface { - type: "errorMigration" - dbFile: string - migrationError: MigrationError - } - - export interface ErrorSQL extends Interface { - type: "errorSQL" - dbFile: string - migrationSQLError: string - } -} - -export type MigrationError = - | MigrationError.MEUpgrade - | MigrationError.MEDowngrade - | MigrationError.MigrationError - -export namespace MigrationError { - export type Tag = "upgrade" | "downgrade" | "migrationError" - - interface Interface { - type: Tag - } - - export interface MEUpgrade extends Interface { - type: "upgrade" - upMigrations: UpMigration - } - - export interface MEDowngrade extends Interface { - type: "downgrade" - downMigrations: [string] - } - - export interface MigrationError extends Interface { - type: "migrationError" - mtrError: MTRError - } -} - -export interface UpMigration { - upName: string - withDown: boolean -} - -export type MTRError = - | MTRError.MTRENoDown - | MTRError.MTREDifferent - -export namespace MTRError { - export type Tag = "noDown" | "different" - - interface Interface { - type: Tag - } - - export interface MTRENoDown extends Interface { - type: "noDown" - upMigrations: UpMigration - } - - export interface MTREDifferent extends Interface { - type: "different" - downMigrations: [string] - } -} diff --git a/packages/simplex-chat-nodejs/tests/core.test.ts b/packages/simplex-chat-nodejs/tests/core.test.ts index dd88a8430b..501c8ec46e 100644 --- a/packages/simplex-chat-nodejs/tests/core.test.ts +++ b/packages/simplex-chat-nodejs/tests/core.test.ts @@ -1,81 +1,85 @@ import * as fs from "fs"; import * as path from "path"; import {core} from "../src/index"; -import {MigrationConfirmation} from "../src/types"; describe("Core tests", () => { - const tmpDir: string = "./tests/tmp"; - const dbPath: string = path.join(tmpDir, "simplex_v1"); + 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 })); it("should initialize chat controller", async () => { - const ctrl: bigint = await core.chatMigrateInit(dbPath, "key", MigrationConfirmation.YesUp); + const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp); expect(typeof ctrl).toBe("bigint"); await expect(core.chatCloseStore(ctrl)).resolves.toBe(undefined); - await expect(core.chatMigrateInit(dbPath, "wrong_key", MigrationConfirmation.YesUp)).rejects.toMatchObject({ + await expect(core.chatMigrateInit(dbPath, "wrong_key", core.MigrationConfirmation.YesUp)).rejects.toMatchObject({ message: "Database or migration error (see dbMigrationError property)", - dbMigrationError: expect.objectContaining({ type: "errorNotADatabase" }) + dbMigrationError: expect.objectContaining({type: "errorNotADatabase"}) }); }); it("should send command and receive event", async () => { - const ctrl: bigint = await core.chatMigrateInit(dbPath, "key", MigrationConfirmation.YesUp); + const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp); - await expect(core.chatSendCmd(ctrl, "/v")).resolves.toHaveProperty("result"); - await expect(core.chatSendCmd(ctrl, '/debug event {"type": "chatSuspended"}')).resolves.toMatchObject({ result: { type: "cmdOk" } }); + await expect(core.chatSendCmd(ctrl, "/v")).resolves.toMatchObject({ + type: "versionInfo" + }); + await expect(core.chatSendCmd(ctrl, '/debug event {"type": "chatSuspended"}')).resolves.toMatchObject({ + type: "cmdOk" + }); - const wait: number = 500_000; - await expect(core.chatRecvMsgWait(ctrl, wait)).resolves.toMatchObject({ result: { type: "chatSuspended" } }); + const wait = 500_000; + await expect(core.chatRecvMsgWait(ctrl, wait)).resolves.toMatchObject({ + type: "chatSuspended" + }); await expect(core.chatRecvMsgWait(ctrl, wait)).resolves.toBe(undefined); - await expect(core.chatSendCmd(ctrl, "/unknown")).resolves.toHaveProperty("error"); + await expect(core.chatSendCmd(ctrl, "/unknown")).rejects.toMatchObject({ + message: "Chat command error (see chatError property)", + chatError: expect.objectContaining({type: "error"}) + }); await core.chatCloseStore(ctrl); }); it("should write/read encrypted file from/to buffer", async () => { - const ctrl: bigint = await core.chatMigrateInit(dbPath, "key", MigrationConfirmation.YesUp); + const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp); - const filePath: string = path.join(tmpDir, "write_file.txt"); - const buffer: ArrayBuffer = new Uint8Array([0, 1, 2]).buffer; - const encRes: { fileKey: string; fileNonce: string } = await core.chatWriteFile(ctrl, filePath, buffer); - const key: string = encRes.fileKey; - const nonce: string = encRes.fileNonce; - expect(typeof key).toBe("string"); - expect(typeof nonce).toBe("string"); + const filePath = path.join(tmpDir, "write_file.txt"); + const buffer = new Uint8Array([0, 1, 2]).buffer; + const cryptoArgs = await core.chatWriteFile(ctrl, filePath, buffer); + expect(typeof cryptoArgs.fileKey).toBe("string"); + expect(typeof cryptoArgs.fileNonce).toBe("string"); - const buffer2: ArrayBuffer = await core.chatReadFile(filePath, key, nonce); + const buffer2 = await core.chatReadFile(filePath, cryptoArgs); expect(Buffer.from(buffer2).equals(Buffer.from(buffer))).toBe(true); await expect(core.chatWriteFile(ctrl, path.join(tmpDir, "unknown", "unknown.txt"), buffer)).rejects.toThrow(); - await expect(core.chatReadFile(path.join(tmpDir, "unknown.txt"), key, nonce)).rejects.toThrow(); + await expect(core.chatReadFile(path.join(tmpDir, "unknown.txt"), cryptoArgs)).rejects.toThrow(); await core.chatCloseStore(ctrl); }); it("should encrypt/decrypt file", async () => { - const ctrl: bigint = await core.chatMigrateInit(dbPath, "key", MigrationConfirmation.YesUp); + const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp); - const unencryptedPath: string = path.join(tmpDir, "file_unencrypted.txt"); + const unencryptedPath = path.join(tmpDir, "file_unencrypted.txt"); fs.writeFileSync(unencryptedPath, "unencrypted\n"); - const encryptedPath: string = path.join(tmpDir, "file_encrypted.txt"); - const encRes: { fileKey: string; fileNonce: string } = await core.chatEncryptFile(ctrl, unencryptedPath, encryptedPath); - const key: string = encRes.fileKey; - const nonce: string = encRes.fileNonce; - expect(typeof key).toBe("string"); - expect(typeof nonce).toBe("string"); + const encryptedPath = path.join(tmpDir, "file_encrypted.txt"); + const cryptoArgs = await core.chatEncryptFile(ctrl, unencryptedPath, encryptedPath); + expect(typeof cryptoArgs.fileKey).toBe("string"); + expect(typeof cryptoArgs.fileNonce).toBe("string"); const decryptedPath: string = path.join(tmpDir, "file_decrypted.txt"); - await expect(core.chatDecryptFile(encryptedPath, key, nonce, decryptedPath)).resolves.toBe(undefined); + await expect(core.chatDecryptFile(encryptedPath, cryptoArgs, decryptedPath)).resolves.toBe(undefined); expect(fs.readFileSync(decryptedPath, "utf8")).toBe("unencrypted\n"); await expect(core.chatEncryptFile(ctrl, path.join(tmpDir, "unknown.txt"), encryptedPath)).rejects.toThrow(); - await expect(core.chatDecryptFile(path.join(tmpDir, "unknown.txt"), key, nonce, decryptedPath)).rejects.toThrow(); + await expect(core.chatDecryptFile(path.join(tmpDir, "unknown.txt"), cryptoArgs, decryptedPath)).rejects.toThrow(); await core.chatCloseStore(ctrl); });