From 1e4be145c0707a2bdee0830498ab58b72f07c6f2 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 10 Jan 2026 14:30:52 +0000 Subject: [PATCH] refactor, fixes --- packages/simplex-chat-nodejs/cpp/simplex.cc | 971 +++++++++--------- .../simplex-chat-nodejs/tests/addon.test.js | 44 +- 2 files changed, 483 insertions(+), 532 deletions(-) diff --git a/packages/simplex-chat-nodejs/cpp/simplex.cc b/packages/simplex-chat-nodejs/cpp/simplex.cc index 4131e58052..07daded2ff 100644 --- a/packages/simplex-chat-nodejs/cpp/simplex.cc +++ b/packages/simplex-chat-nodejs/cpp/simplex.cc @@ -5,564 +5,523 @@ #include #include "simplex.h" -namespace simplex -{ +namespace simplex { - using namespace Napi; +using namespace Napi; - void haskell_init() - { - int argc = 5; - const char *argv[] = { - "simplex", - "+RTS", // requires `hs_init_with_rtsopts` - "-A64m", // chunk size for new allocations - "-H64m", // initial heap size - "-xn", // non-moving GC - nullptr}; - char **pargv = const_cast(argv); - hs_init_with_rtsopts(&argc, &pargv); +void haskell_init() { + int argc = 5; + const char *argv[] = { + "simplex", + "+RTS", // requires `hs_init_with_rtsopts` + "-A64m", // chunk size for new allocations + "-H64m", // initial heap size + "-xn", // non-moving GC + nullptr}; + char **pargv = const_cast(argv); + 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 { + public: + using ExecuteFn = std::function; + using ResultProcessor = std::function; + + JsonAsyncWorker(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 { + execute_fn_(this); } - 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)}); + void OnOK() override { + HandleScope scope(Env()); + if (result_processor_) { + result_processor_(this, Env()); + } else { + Callback().Call({Env().Null(), String::New(Env(), result_)}); + } } - class GenericAsyncWorker : public AsyncWorker - { - public: - GenericAsyncWorker(Function& callback, std::function execute_lambda) - : AsyncWorker(callback), execute_lambda_(std::move(execute_lambda)) {} + void OnError(const Error& e) override { + HandleScope scope(Env()); + Callback().Call({e.Value(), Env().Undefined()}); + } - void Execute() override { - execute_lambda_(this); + void SetResult(std::string result) { + result_ = std::move(result); + } + + void SetWorkerError(const std::string& msg) { + SetError(msg); + } + + const std::string& GetStringResult() const { + 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; + } + + uintptr_t GetCtrl() const { + return ctrl_; + } + + protected: + std::string result_; + uintptr_t ctrl_ = 0; + + private: + ExecuteFn execute_fn_; + ResultProcessor result_processor_; +}; + +class BinaryAsyncWorker : public AsyncWorker { + public: + using ExecuteFn = std::function; + + BinaryAsyncWorker(Function& callback, ExecuteFn execute_fn) + : AsyncWorker(callback), execute_fn_(std::move(execute_fn)) {} + + void Execute() override { + execute_fn_(this); + } + + void OnOK() override { + HandleScope scope(Env()); + if (original_buf == nullptr || binary_len == 0) { + Callback().Call({Env().Null(), Env().Undefined()}); + return; + } + char* data_ptr = original_buf + 5; + auto finalizer = [](Napi::Env env, char* finalize_data, char* orig) { + free(orig); + }; + Napi::Buffer buffer = Napi::Buffer::New(Env(), data_ptr, binary_len, finalizer, original_buf); + Callback().Call({Env().Null(), buffer}); + } + + void OnError(const Error& e) override { + HandleScope scope(Env()); + Callback().Call({e.Value(), Env().Undefined()}); + } + + void SetWorkerError(const std::string& msg) { + SetError(msg); + } + + char* original_buf = nullptr; + size_t binary_len = 0; + + private: + ExecuteFn execute_fn_; +}; + +// Helper for converting chat_ctrl pointer to BigInt +Napi::BigInt ToChatCtrlBigInt(Napi::Env env, uintptr_t ctrl) { + return Napi::BigInt::New(env, static_cast(ctrl)); +} + +// Helper for converting BigInt to chat_ctrl pointer +chat_ctrl FromChatCtrlBigInt(const Napi::Value& value) { + Napi::Env env = value.Env(); + if (!value.IsBigInt()) { + Napi::TypeError::New(env, "Expected BigInt for ctrl").ThrowAsJavaScriptException(); + return nullptr; + } + Napi::BigInt big = value.As(); + bool lossless; + uint64_t val = big.Uint64Value(&lossless); + if (!lossless) { + Napi::TypeError::New(env, "BigInt too large for ctrl").ThrowAsJavaScriptException(); + return nullptr; + } + 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) { + 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); +} + +Napi::Promise CreatePromiseAndCallback(Env env, Function& cb_out) { + Promise::Deferred deferred = Promise::Deferred::New(env); + cb_out = Function::New(env, [deferred](const CallbackInfo& args) { + if (!args[0].IsNull() && !args[0].IsUndefined()) { + deferred.Reject(args[0]); + } else { + deferred.Resolve(args[1]); + } + }); + return deferred.Promise(); +} + +// 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}); + } + }; +} - void OnOK() override { - HandleScope scope(Env()); - std::vector args = { Env().Null() }; - auto res = GetResult(Env()); - args.insert(args.end(), res.begin(), res.end()); - Callback().Call(args); - } +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()}); + } + }; +} - void OnError(const Error& e) override { - HandleScope scope(Env()); - Callback().Call({e.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()}); + } + }; +} - virtual std::vector GetResult(Napi::Env env) override { - return { String::New(env, result_) }; - } +// Refactored functions using common patterns - void SetResult(std::string result) { - result_ = std::move(result); - } +Value ChatMigrateInit(const CallbackInfo& args) { + Env env = args.Env(); + if (args.Length() < 3 || !args[0].IsString() || !args[1].IsString() || !args[2].IsString()) { + TypeError::New(env, "Expected three string arguments").ThrowAsJavaScriptException(); + return env.Undefined(); + } - void SetWorkerError(const std::string& msg) { - SetError(msg); - } + std::string path = args[0].As().Utf8Value(); + std::string key = args[1].As().Utf8Value(); + std::string confirm = args[2].As().Utf8Value(); - protected: - std::string result_; + Function cb; + Promise promise = CreatePromiseAndCallback(env, cb); - private: - std::function execute_lambda_; + auto execute_fn = [path, key, confirm](JsonAsyncWorker* 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); }; - class JsonAsyncWorker : public GenericAsyncWorker - { - public: - JsonAsyncWorker(Function& callback, std::function execute_lambda) - : GenericAsyncWorker(callback, std::move(execute_lambda)) {} + JsonAsyncWorker* worker = new JsonAsyncWorker(cb, std::move(execute_fn), MigrateResultProcessor()); + worker->Queue(); - void OnOK() override { - HandleScope scope(Env()); - Value parsed = ParseJson(Env(), result_); - if (Env().IsExceptionPending()) { - Error exception = Env().GetAndClearPendingException(); - Callback().Call({exception.Value(), Env().Undefined()}); - return; - } - Callback().Call({Env().Null(), parsed}); - } + return promise; +} + +Value ChatCloseStore(const CallbackInfo& args) { + Env env = args.Env(); + if (args.Length() < 1 || !args[0].IsBigInt()) { + TypeError::New(env, "Expected bigint (ctrl)").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + chat_ctrl ctrl = FromChatCtrlBigInt(args[0]); + + Function cb; + Promise promise = CreatePromiseAndCallback(env, cb); + + auto execute_fn = [ctrl](JsonAsyncWorker* 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); }; - class ResultUnwrapAsyncWorker : public GenericAsyncWorker - { - public: - ResultUnwrapAsyncWorker(Function& callback, std::function execute_lambda) - : GenericAsyncWorker(callback, std::move(execute_lambda)) {} + JsonAsyncWorker* worker = new JsonAsyncWorker(cb, std::move(execute_fn), ErrorResultProcessor()); + worker->Queue(); - void OnOK() override { - HandleScope scope(Env()); - Value parsed = ParseJson(Env(), result_); - if (Env().IsExceptionPending()) { - Error exception = Env().GetAndClearPendingException(); - Callback().Call({exception.Value(), Env().Undefined()}); - 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"); - 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); - Callback().Call({err.Value(), Env().Undefined()}); - } else { - Value cryptoArgs = parsed_obj.Get("cryptoArgs"); - if (cryptoArgs.IsUndefined()) { - Error err = Error::New(Env(), "Missing cryptoArgs"); - Callback().Call({err.Value(), Env().Undefined()}); - return; - } - Callback().Call({Env().Null(), cryptoArgs}); - } - } + return promise; +} + +Value ChatSendCmd(const CallbackInfo& args) { + Env env = args.Env(); + if (args.Length() < 2 || !args[0].IsBigInt() || !args[1].IsString()) { + TypeError::New(env, "Expected bigint (ctrl) and string (cmd)").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + chat_ctrl ctrl = FromChatCtrlBigInt(args[0]); + std::string cmd = args[1].As().Utf8Value(); + + Function cb; + Promise promise = CreatePromiseAndCallback(env, cb); + + auto execute_fn = [ctrl, cmd](JsonAsyncWorker* worker) { + char* c_res = chat_send_cmd(ctrl, cmd.c_str()); + HandleCResult(worker, c_res, "chat_send_cmd"); }; - class MigrateAsyncWorker : public GenericAsyncWorker - { - public: - MigrateAsyncWorker(Function& callback, std::function execute_lambda) - : GenericAsyncWorker(callback, execute_lambda) {} + JsonAsyncWorker* worker = new JsonAsyncWorker(cb, std::move(execute_fn), JsonResultProcessor()); + worker->Queue(); - void SetCtrl(uintptr_t ctrl) { - ctrl_ = ctrl; - } + return promise; +} - void OnOK() override { - HandleScope scope(Env()); - Value parsed = ParseJson(Env(), result_); - if (Env().IsExceptionPending()) { - Error exception = Env().GetAndClearPendingException(); - Callback().Call({exception.Value(), Env().Undefined()}); - return; - } - Object parsed_obj = parsed.As(); - Value type_val = parsed_obj.Get("type"); - if (type_val.IsString() && type_val.As().Utf8Value() == "ok") { - Callback().Call({Env().Null(), BigInt::New(Env(), static_cast(ctrl_))}); - } else { - Error err = Error::New(Env(), "Database or migration error (see dbMigrationError property)"); - err.Set("dbMigrationError", parsed_obj); - Callback().Call({err.Value(), Env().Undefined()}); - } - } +Value ChatRecvMsgWait(const CallbackInfo& args) { + Env env = args.Env(); + if (args.Length() < 2 || !args[0].IsBigInt() || !args[1].IsNumber()) { + TypeError::New(env, "Expected bigint (ctrl), number (wait)").ThrowAsJavaScriptException(); + return env.Undefined(); + } - private: - uintptr_t ctrl_ = 0; + chat_ctrl ctrl = FromChatCtrlBigInt(args[0]); + int wait = static_cast(args[1].As().Int32Value()); + + Function cb; + Promise promise = CreatePromiseAndCallback(env, cb); + + auto execute_fn = [ctrl, wait](JsonAsyncWorker* 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); }; - class BinaryAsyncWorker : public AsyncWorker - { - public: - BinaryAsyncWorker(Function& callback, std::function execute_lambda) - : AsyncWorker(callback), execute_lambda_(std::move(execute_lambda)) {} + JsonAsyncWorker* worker = new JsonAsyncWorker(cb, std::move(execute_fn), RecvResultProcessor()); + worker->Queue(); - void Execute() override { - execute_lambda_(this); - } + return promise; +} - void OnOK() override { - HandleScope scope(Env()); - char* data_ptr = original_buf + 5; - auto finalizer = [](Napi::Env env, char* finalize_data, char* orig) { - free(orig); - }; - Napi::Buffer buffer = Napi::Buffer::New(Env(), data_ptr, binary_len, finalizer, original_buf); - Callback().Call({Env().Null(), buffer}); - } +Value ChatWriteFile(const CallbackInfo& args) { + Env env = args.Env(); + if (args.Length() < 3 || !args[0].IsBigInt() || !args[1].IsString() || !args[2].IsArrayBuffer()) { + TypeError::New(env, "Expected bigint (ctrl), string (path), ArrayBuffer").ThrowAsJavaScriptException(); + return env.Undefined(); + } - void OnError(const Error& e) override { - HandleScope scope(Env()); - Callback().Call({e.Value(), Env().Undefined()}); - } + chat_ctrl ctrl = FromChatCtrlBigInt(args[0]); + std::string path = args[1].As().Utf8Value(); + ArrayBuffer ab = args[2].As(); + char* data = static_cast(ab.Data()); + int len = static_cast(ab.ByteLength()); - void SetWorkerError(const std::string& msg) { - SetError(msg); - } + Function cb; + Promise promise = CreatePromiseAndCallback(env, cb); - public: - char* original_buf = nullptr; - size_t binary_len = 0; - - private: - std::function execute_lambda_; + auto execute_fn = [ctrl, path, data, len](JsonAsyncWorker* worker) { + char* c_res = chat_write_file(ctrl, path.c_str(), data, len); + HandleCResult(worker, c_res, "chat_write_file"); }; - class WriteAsyncWorker : public ResultUnwrapAsyncWorker - { - public: - WriteAsyncWorker(Function& callback, std::function execute_lambda, ArrayBuffer ab) - : ResultUnwrapAsyncWorker(callback, std::move(execute_lambda)), ab_ref(Reference::New(ab, 1)) {} + // 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()); + worker->Queue(); - Reference ab_ref; + return promise; +} + +Value ChatReadFile(const CallbackInfo& args) { + Env env = args.Env(); + if (args.Length() < 3 || !args[0].IsString() || !args[1].IsString() || !args[2].IsString()) { + TypeError::New(env, "Expected three strings (path, key, nonce)").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + std::string path = args[0].As().Utf8Value(); + std::string key = args[1].As().Utf8Value(); + std::string nonce = args[2].As().Utf8Value(); + + Function cb; + Promise promise = CreatePromiseAndCallback(env, cb); + + auto execute_fn = [path, key, nonce](BinaryAsyncWorker* worker) { + char* buf = chat_read_file(path.c_str(), key.c_str(), nonce.c_str()); + if (buf == nullptr) { + worker->SetWorkerError("chat_read_file failed"); + return; + } + char status = buf[0]; + if (status == 1) { + std::string err = buf + 1; + free(buf); + worker->SetWorkerError(err); + return; + } else if (status == 0) { + uint32_t len = *(uint32_t*)(buf + 1); + worker->original_buf = buf; + worker->binary_len = len; + } else { + free(buf); + worker->SetWorkerError("Unexpected status from chat_read_file"); + return; + } }; - Napi::Promise CreatePromiseAndCallback(Env env, Function& cb_out) { - Promise::Deferred deferred = Promise::Deferred::New(env); - cb_out = Function::New(env, [deferred](const CallbackInfo& args) { - if (!args[0].IsNull() && !args[0].IsUndefined()) { - deferred.Reject(args[0]); - } else { - deferred.Resolve(args[1]); - } - }); - return deferred.Promise(); + BinaryAsyncWorker* worker = new BinaryAsyncWorker(cb, std::move(execute_fn)); + worker->Queue(); + + return promise; +} + +Value ChatEncryptFile(const CallbackInfo& args) { + Env env = args.Env(); + if (args.Length() < 3 || !args[0].IsBigInt() || !args[1].IsString() || !args[2].IsString()) { + TypeError::New(env, "Expected bigint (ctrl), two strings (fromPath, toPath)").ThrowAsJavaScriptException(); + return env.Undefined(); } - Value ChatMigrateInit(const CallbackInfo& args) - { - Env env = args.Env(); - if (args.Length() < 3 || !args[0].IsString() || !args[1].IsString() || !args[2].IsString()) { - TypeError::New(env, "Expected three string arguments").ThrowAsJavaScriptException(); - return env.Undefined(); - } + chat_ctrl ctrl = FromChatCtrlBigInt(args[0]); + std::string fromPath = args[1].As().Utf8Value(); + std::string toPath = args[2].As().Utf8Value(); - std::string path = args[0].As().Utf8Value(); - std::string key = args[1].As().Utf8Value(); - std::string confirm = args[2].As().Utf8Value(); + Function cb; + Promise promise = CreatePromiseAndCallback(env, cb); - Function cb; - Promise promise = CreatePromiseAndCallback(env, cb); - - auto execute_lambda = [path, key, confirm](GenericAsyncWorker* 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); - auto mworker = static_cast(worker); - mworker->SetCtrl(reinterpret_cast(ctrl)); - worker->SetResult(res); - }; - - MigrateAsyncWorker* worker = new MigrateAsyncWorker(cb, std::move(execute_lambda)); - worker->Queue(); - - return promise; - } - - class CloseStoreAsyncWorker : public GenericAsyncWorker - { - public: - CloseStoreAsyncWorker(Function& callback, std::function execute_lambda) - : GenericAsyncWorker(callback, std::move(execute_lambda)) {} - - void OnOK() override { - HandleScope scope(Env()); - if (result_.empty()) { - Callback().Call({Env().Null(), Env().Undefined()}); - } else { - Error err = Error::New(Env(), result_); - Callback().Call({err.Value(), Env().Undefined()}); - } - } + auto execute_fn = [ctrl, fromPath, toPath](JsonAsyncWorker* worker) { + char* c_res = chat_encrypt_file(ctrl, fromPath.c_str(), toPath.c_str()); + HandleCResult(worker, c_res, "chat_encrypt_file"); }; - Value ChatCloseStore(const CallbackInfo& args) - { - Env env = args.Env(); - if (args.Length() < 1 || !args[0].IsBigInt()) { - TypeError::New(env, "Expected bigint (ctrl)").ThrowAsJavaScriptException(); - return env.Undefined(); - } + JsonAsyncWorker* worker = new JsonAsyncWorker(cb, std::move(execute_fn), WriteResultProcessor()); + worker->Queue(); - bool lossless; - chat_ctrl ctrl = reinterpret_cast(args[0].As().Int64Value(&lossless)); + return promise; +} - if (!lossless) { - TypeError::New(env, "BigInt too large for ctrl").ThrowAsJavaScriptException(); - return env.Undefined(); - } - - Function cb; - Promise promise = CreatePromiseAndCallback(env, cb); - - auto execute_lambda = [ctrl](GenericAsyncWorker* 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); - }; - - CloseStoreAsyncWorker* worker = new CloseStoreAsyncWorker(cb, std::move(execute_lambda)); - worker->Queue(); - - return promise; +Value ChatDecryptFile(const CallbackInfo& args) { + Env env = args.Env(); + if (args.Length() < 4 || !args[0].IsString() || !args[1].IsString() || !args[2].IsString() || !args[3].IsString()) { + TypeError::New(env, "Expected four strings (fromPath, key, nonce, toPath)").ThrowAsJavaScriptException(); + return env.Undefined(); } - Value ChatSendCmd(const CallbackInfo& args) - { - Env env = args.Env(); - if (args.Length() < 2 || !args[0].IsBigInt() || !args[1].IsString()) { - TypeError::New(env, "Expected bigint (ctrl) and string (cmd)").ThrowAsJavaScriptException(); - return env.Undefined(); + std::string fromPath = args[0].As().Utf8Value(); + std::string key = args[1].As().Utf8Value(); + std::string nonce = args[2].As().Utf8Value(); + std::string toPath = args[3].As().Utf8Value(); + + Function cb; + Promise promise = CreatePromiseAndCallback(env, cb); + + auto execute_fn = [fromPath, key, nonce, toPath](JsonAsyncWorker* 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); + }; - bool lossless; - chat_ctrl ctrl = reinterpret_cast(args[0].As().Int64Value(&lossless)); - if (!lossless) { - TypeError::New(env, "BigInt too large for ctrl").ThrowAsJavaScriptException(); - return env.Undefined(); - } - std::string cmd = args[1].As().Utf8Value(); + JsonAsyncWorker* worker = new JsonAsyncWorker(cb, std::move(execute_fn), ErrorResultProcessor()); + worker->Queue(); - Function cb; - Promise promise = CreatePromiseAndCallback(env, cb); + return promise; +} - auto execute_lambda = [ctrl, cmd](GenericAsyncWorker* worker) { - char* c_res = chat_send_cmd(ctrl, cmd.c_str()); - if (c_res == nullptr) { - worker->SetWorkerError("chat_send_cmd failed"); - return; - } - std::string res = c_res; - free(c_res); - if (res.empty()) { - worker->SetWorkerError("chat_send_cmd failed"); - return; - } - worker->SetResult(res); - }; +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)); + return exports; +} - JsonAsyncWorker* worker = new JsonAsyncWorker(cb, std::move(execute_lambda)); - worker->Queue(); - - return promise; - } - - Value ChatRecvMsgWait(const CallbackInfo& args) - { - Env env = args.Env(); - if (args.Length() < 2 || !args[0].IsBigInt() || !args[1].IsNumber()) { - TypeError::New(env, "Expected bigint (ctrl), number (wait)").ThrowAsJavaScriptException(); - return env.Undefined(); - } - - bool lossless; - chat_ctrl ctrl = reinterpret_cast(args[0].As().Int64Value(&lossless)); - if (!lossless) { - TypeError::New(env, "BigInt too large for ctrl").ThrowAsJavaScriptException(); - return env.Undefined(); - } - int wait = static_cast(args[1].As().Int32Value()); - - Function cb; - Promise promise = CreatePromiseAndCallback(env, cb); - - auto execute_lambda = [ctrl, wait](GenericAsyncWorker* 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); - if (res.empty()) { - worker->SetWorkerError("chat_recv_msg_wait failed"); - return; - } - worker->SetResult(res); - }; - - JsonAsyncWorker* worker = new JsonAsyncWorker(cb, std::move(execute_lambda)); - worker->Queue(); - - return promise; - } - - Value ChatWriteFile(const CallbackInfo& args) - { - Env env = args.Env(); - if (args.Length() < 3 || !args[0].IsBigInt() || !args[1].IsString() || !args[2].IsArrayBuffer()) { - TypeError::New(env, "Expected bigint (ctrl), string (path), ArrayBuffer").ThrowAsJavaScriptException(); - return env.Undefined(); - } - - bool lossless; - chat_ctrl ctrl = reinterpret_cast(args[0].As().Int64Value(&lossless)); - if (!lossless) { - TypeError::New(env, "BigInt too large for ctrl").ThrowAsJavaScriptException(); - return env.Undefined(); - } - std::string path = args[1].As().Utf8Value(); - ArrayBuffer ab = args[2].As(); - char* data = static_cast(ab.Data()); - int len = static_cast(ab.ByteLength()); - - Function cb; - Promise promise = CreatePromiseAndCallback(env, cb); - - auto execute_lambda = [ctrl, path, data, len](GenericAsyncWorker* worker) { - char* c_res = chat_write_file(ctrl, path.c_str(), data, len); - if (c_res == nullptr) { - worker->SetWorkerError("chat_write_file failed"); - return; - } - std::string res = c_res; - free(c_res); - if (res.empty()) { - worker->SetWorkerError("chat_write_file failed"); - return; - } - worker->SetResult(res); - }; - - WriteAsyncWorker* worker = new WriteAsyncWorker(cb, std::move(execute_lambda), ab); - worker->Queue(); - - return promise; - } - - Value ChatReadFile(const CallbackInfo& args) - { - Env env = args.Env(); - if (args.Length() < 3 || !args[0].IsString() || !args[1].IsString() || !args[2].IsString()) { - TypeError::New(env, "Expected three strings (path, key, nonce)").ThrowAsJavaScriptException(); - return env.Undefined(); - } - - std::string path = args[0].As().Utf8Value(); - std::string key = args[1].As().Utf8Value(); - std::string nonce = args[2].As().Utf8Value(); - - Function cb; - Promise promise = CreatePromiseAndCallback(env, cb); - - auto execute_lambda = [path, key, nonce](BinaryAsyncWorker* worker) { - char* buf = chat_read_file(path.c_str(), key.c_str(), nonce.c_str()); - if (buf == nullptr) { - worker->SetWorkerError("chat_read_file failed"); - return; - } - char status = buf[0]; - if (status == 1) { - std::string err = buf + 1; - free(buf); - worker->SetWorkerError(err); - return; - } else if (status == 0) { - uint32_t len = *(uint32_t*)(buf + 1); - worker->original_buf = buf; - worker->binary_len = len; - } else { - free(buf); - worker->SetWorkerError("Unexpected status from chat_read_file"); - return; - } - }; - - BinaryAsyncWorker* worker = new BinaryAsyncWorker(cb, std::move(execute_lambda)); - worker->Queue(); - - return promise; - } - - Value ChatEncryptFile(const CallbackInfo& args) - { - Env env = args.Env(); - if (args.Length() < 3 || !args[0].IsBigInt() || !args[1].IsString() || !args[2].IsString()) { - TypeError::New(env, "Expected bigint (ctrl), two strings (fromPath, toPath)").ThrowAsJavaScriptException(); - return env.Undefined(); - } - - bool lossless; - chat_ctrl ctrl = reinterpret_cast(args[0].As().Int64Value(&lossless)); - if (!lossless) { - TypeError::New(env, "BigInt too large for ctrl").ThrowAsJavaScriptException(); - return env.Undefined(); - } - std::string fromPath = args[1].As().Utf8Value(); - std::string toPath = args[2].As().Utf8Value(); - - Function cb; - Promise promise = CreatePromiseAndCallback(env, cb); - - auto execute_lambda = [ctrl, fromPath, toPath](GenericAsyncWorker* worker) { - char* c_res = chat_encrypt_file(ctrl, fromPath.c_str(), toPath.c_str()); - if (c_res == nullptr) { - worker->SetWorkerError("chat_encrypt_file failed"); - return; - } - std::string res = c_res; - free(c_res); - if (res.empty()) { - worker->SetWorkerError("chat_encrypt_file failed"); - return; - } - worker->SetResult(res); - }; - - ResultUnwrapAsyncWorker* worker = new ResultUnwrapAsyncWorker(cb, std::move(execute_lambda)); - worker->Queue(); - - return promise; - } - - Value ChatDecryptFile(const CallbackInfo& args) - { - Env env = args.Env(); - if (args.Length() < 4 || !args[0].IsString() || !args[1].IsString() || !args[2].IsString() || !args[3].IsString()) { - TypeError::New(env, "Expected four strings (fromPath, key, nonce, toPath)").ThrowAsJavaScriptException(); - return env.Undefined(); - } - - std::string fromPath = args[0].As().Utf8Value(); - std::string key = args[1].As().Utf8Value(); - std::string nonce = args[2].As().Utf8Value(); - std::string toPath = args[3].As().Utf8Value(); - - Function cb; - Promise promise = CreatePromiseAndCallback(env, cb); - - auto execute_lambda = [fromPath, key, nonce, toPath](GenericAsyncWorker* worker) { - char* c_res = chat_decrypt_file(fromPath.c_str(), key.c_str(), nonce.c_str(), toPath.c_str()); - std::string res = c_res ? c_res : ""; - free(c_res); - if (!res.empty()) { - worker->SetWorkerError(res); - return; - } - worker->SetResult("ok"); - }; - - GenericAsyncWorker* worker = new GenericAsyncWorker(cb, std::move(execute_lambda)); - worker->Queue(); - - return promise; - } - - 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)); - return exports; - } - - NODE_API_MODULE(simplex, Init) +NODE_API_MODULE(simplex, Init) } diff --git a/packages/simplex-chat-nodejs/tests/addon.test.js b/packages/simplex-chat-nodejs/tests/addon.test.js index 13fb458ad0..9bcb90afe9 100644 --- a/packages/simplex-chat-nodejs/tests/addon.test.js +++ b/packages/simplex-chat-nodejs/tests/addon.test.js @@ -6,13 +6,9 @@ describe('Addon Tests', () => { const tmpDir = './tests/tmp'; const dbPath = path.join(tmpDir, 'simplex_v1'); - beforeEach(() => { - fs.mkdirSync(tmpDir, {recursive: true}); - }); + beforeEach(() => fs.mkdirSync(tmpDir, {recursive: true})); - afterEach(() => { - fs.rmSync(tmpDir, {recursive: true, force: true}); - }); + afterEach(() => fs.rmSync(tmpDir, {recursive: true, force: true})); it('should initialize controller', async () => { const ctrl = await core.chatMigrateInit(dbPath, "key", "yesUp"); @@ -32,17 +28,15 @@ describe('Addon Tests', () => { it('should send command and receive event', async () => { const ctrl = await core.chatMigrateInit(dbPath, "key", "yesUp"); - const sendRes1 = await core.chatSendCmd(ctrl, "/v"); - expect(typeof sendRes1).toBe('object'); - expect(sendRes1).toHaveProperty('result') + await expect(core.chatSendCmd(ctrl, "/v")).resolves.toHaveProperty('result'); + await expect(core.chatSendCmd(ctrl, '/debug event {"type": "chatSuspended"}')).resolves.toMatchObject({result: {type: 'cmdOk'}}); - const sendRes2 = await core.chatSendCmd(ctrl, '/debug event {"type": "chatSuspended"}'); - expect(sendRes2).toMatchObject({result: {type: 'cmdOk'}}); - - const wait = 15_000_000; - const recvRes = await core.chatRecvMsgWait(ctrl, wait); - expect(recvRes).toMatchObject({result: {type: 'chatSuspended'}}); + const wait = 500_000; + await expect(core.chatRecvMsgWait(ctrl, wait)).resolves.toMatchObject({result: {type: 'chatSuspended'}}); + await expect(core.chatRecvMsgWait(ctrl, wait)).resolves.toBe(undefined); + await expect(core.chatSendCmd(ctrl, '/unknown')).resolves.toHaveProperty('error'); + await core.chatCloseStore(ctrl); }); @@ -51,17 +45,17 @@ describe('Addon Tests', () => { const filePath = path.join(tmpDir, 'write_file.txt'); const buffer = new Uint8Array([0, 1, 2]).buffer; - const encRes1 = await core.chatWriteFile(ctrl, filePath, buffer); - const key1 = encRes1.fileKey; - const nonce1 = encRes1.fileNonce; - expect(typeof key1).toBe('string'); - expect(typeof nonce1).toBe('string'); + const encRes = await core.chatWriteFile(ctrl, filePath, buffer); + const key = encRes.fileKey; + const nonce = encRes.fileNonce; + expect(typeof key).toBe('string'); + expect(typeof nonce).toBe('string'); - const buffer2 = await core.chatReadFile(filePath, key1, nonce1); + const buffer2 = await core.chatReadFile(filePath, key, nonce); 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'), key1, nonce1)).rejects.toThrow(); + await expect(core.chatReadFile(path.join(tmpDir, 'unknown.txt'), key, nonce)).rejects.toThrow(); core.chatCloseStore(ctrl); }); @@ -79,11 +73,9 @@ describe('Addon Tests', () => { expect(typeof nonce).toBe('string'); const decryptedPath = path.join(tmpDir, 'file_decrypted.txt'); - const decryptRes = await core.chatDecryptFile(encryptedPath, key, nonce, decryptedPath); - expect(decryptRes).toBeDefined(); // Assuming it returns something like 'ok' + await expect(core.chatDecryptFile(encryptedPath, key, nonce, decryptedPath)).resolves.toBe(undefined); - const decryptedContent = fs.readFileSync(decryptedPath, 'utf8'); - expect(decryptedContent).toBe("unencrypted\n"); + 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();