From 80182a723744165062d32e93e32eab059eee63ad Mon Sep 17 00:00:00 2001 From: shum Date: Fri, 25 Sep 2026 12:51:14 +0000 Subject: [PATCH] nodejs: load libsimplex at runtime per backend --- packages/simplex-chat-nodejs/.gitignore | 1 - packages/simplex-chat-nodejs/.npmignore | 1 - packages/simplex-chat-nodejs/README.md | 22 +- packages/simplex-chat-nodejs/binding.gyp | 30 +- packages/simplex-chat-nodejs/cpp/simplex.cc | 174 +++++++++++- packages/simplex-chat-nodejs/cpp/simplex.h | 32 +-- .../docs/Namespace.core.md | 2 + .../docs/api.Class.ChatApi.md | 148 +++++----- .../docs/api.TypeAlias.DbConfig.md | 8 +- .../docs/core.Class.ChatAPIError.md | 8 +- .../docs/core.Class.ChatInitError.md | 8 +- ...MigrationError.Interface.ErrorMigration.md | 8 +- ...rationError.Interface.ErrorNotADatabase.md | 6 +- ...ore.DBMigrationError.Interface.ErrorSQL.md | 8 +- ...tionError.Interface.InvalidConfirmation.md | 4 +- ...grationError.Interface.InvalidQueueSize.md | 4 +- .../core.DBMigrationError.TypeAlias.Tag.md | 2 +- .../core.Enumeration.MigrationConfirmation.md | 10 +- .../docs/core.Function.chatCloseStore.md | 2 +- .../docs/core.Function.chatDecryptFile.md | 2 +- .../docs/core.Function.chatEncryptFile.md | 2 +- .../docs/core.Function.chatMigrateInit.md | 2 +- .../docs/core.Function.chatReadFile.md | 2 +- .../docs/core.Function.chatRecvMsgWait.md | 2 +- .../docs/core.Function.chatSendCmd.md | 2 +- .../docs/core.Function.chatWriteFile.md | 2 +- .../docs/core.Function.loadLibrary.md | 24 ++ .../docs/core.Interface.APIResult.md | 6 +- .../docs/core.Interface.CryptoArgs.md | 6 +- .../docs/core.Interface.UpMigration.md | 6 +- .../core.MTRError.Interface.MTREDifferent.md | 6 +- .../core.MTRError.Interface.MTRENoDown.md | 6 +- .../docs/core.MTRError.TypeAlias.Tag.md | 2 +- ...re.MigrationError.Interface.MEDowngrade.md | 6 +- ...core.MigrationError.Interface.MEUpgrade.md | 6 +- ...MigrationError.Interface.MigrationError.md | 6 +- .../docs/core.MigrationError.TypeAlias.Tag.md | 2 +- .../docs/core.TypeAlias.Backend.md | 11 + .../docs/core.TypeAlias.DBMigrationError.md | 2 +- .../docs/core.TypeAlias.MTRError.md | 2 +- .../docs/core.TypeAlias.MigrationError.md | 2 +- packages/simplex-chat-nodejs/package.json | 3 +- packages/simplex-chat-nodejs/src/api.ts | 9 +- packages/simplex-chat-nodejs/src/core.ts | 19 ++ .../simplex-chat-nodejs/src/download-libs.js | 266 ------------------ packages/simplex-chat-nodejs/src/simplex.d.ts | 1 + .../tests/api.unit.test.ts | 23 ++ .../simplex-chat-nodejs/tests/core.test.ts | 41 +++ .../simplex-chat-nodejs/tests/loader.test.ts | 97 +++++++ 49 files changed, 549 insertions(+), 495 deletions(-) create mode 100644 packages/simplex-chat-nodejs/docs/core.Function.loadLibrary.md create mode 100644 packages/simplex-chat-nodejs/docs/core.TypeAlias.Backend.md delete mode 100644 packages/simplex-chat-nodejs/src/download-libs.js create mode 100644 packages/simplex-chat-nodejs/tests/loader.test.ts diff --git a/packages/simplex-chat-nodejs/.gitignore b/packages/simplex-chat-nodejs/.gitignore index 322e38bfda..096404c64c 100644 --- a/packages/simplex-chat-nodejs/.gitignore +++ b/packages/simplex-chat-nodejs/.gitignore @@ -2,7 +2,6 @@ node_modules/ package-lock.json .vscode build/ -libs/ dist/ coverage/ tmp/ diff --git a/packages/simplex-chat-nodejs/.npmignore b/packages/simplex-chat-nodejs/.npmignore index 26fdd85dff..3e2e84b087 100644 --- a/packages/simplex-chat-nodejs/.npmignore +++ b/packages/simplex-chat-nodejs/.npmignore @@ -1,3 +1,2 @@ -libs/ build/ node_modules/ diff --git a/packages/simplex-chat-nodejs/README.md b/packages/simplex-chat-nodejs/README.md index 739b41b34e..f750dce1b2 100644 --- a/packages/simplex-chat-nodejs/README.md +++ b/packages/simplex-chat-nodejs/README.md @@ -62,31 +62,27 @@ There is an example with more options in [./examples/squaring-bot.ts](./examples You can run it with: `npx ts-node ./examples/squaring-bot.ts` -## PostgreSQL backend +## Native library -By default, the package uses SQLite. To use PostgreSQL instead: +`libsimplex` is downloaded on first use into the user cache (`$XDG_CACHE_HOME/simplex-chat` or `~/.cache/simplex-chat` on Linux, `~/Library/Caches/simplex-chat` on macOS, `%LOCALAPPDATA%\simplex-chat` on Windows), shared with the Python library. To download it ahead of time, for example in a Dockerfile, run as the user that runs the app: ```bash -npm install simplex-chat --simplex_backend=postgres +npx simplex-chat install # sqlite (default) +npx simplex-chat install --backend postgres # Linux x86_64 only ``` -Or persist the setting in `.npmrc`: +Set `SIMPLEX_LIBS_DIR` to a directory with a local build of `libsimplex` and its dependencies to use it instead; it must be built for the backend in use. -```ini -simplex_backend=postgres -``` +`ChatApi.init` loads the library for `DbConfig.type`; the low-level `core` functions require `core.loadLibrary(backend)` first. -### Prerequisites (PostgreSQL) +## PostgreSQL backend + +`DbConfig.type` selects the backend; one backend per process. - `libpq5` must be installed on the host system (`apt install libpq5` on Debian/Ubuntu) - PostgreSQL backend is only available for Linux x86_64 - A PostgreSQL server accessible via connection string -### Passing PostgreSQL connection - -The `DbConfig` type is a discriminated union — pick the variant that matches -the backend you installed: - ```ts // SQLite (default) dbOpts: {type: "sqlite", filePrefix: "./data/bot"} diff --git a/packages/simplex-chat-nodejs/binding.gyp b/packages/simplex-chat-nodejs/binding.gyp index cfa6d61039..de6dbfbfd4 100644 --- a/packages/simplex-chat-nodejs/binding.gyp +++ b/packages/simplex-chat-nodejs/binding.gyp @@ -15,36 +15,8 @@ "msvs_settings": { "VCCLCompilerTool": { "ExceptionHandling": 1 } }, "defines": [ "NAPI_DISABLE_CPP_EXCEPTIONS" ], "conditions": [ - ["OS=='mac'", { - "libraries": [ - "-L<(module_root_dir)/libs", - "-lsimplex" - ], - "xcode_settings": { - "OTHER_LDFLAGS": [ - "-Wl,-rpath,@loader_path/../../libs" - ] - } - }], ["OS=='linux'", { - "libraries": [ - "-L<(module_root_dir)/libs", - "-lsimplex" - ], - "ldflags": [ - "-Wl,-rpath,'$$ORIGIN'/../../libs" - ] - }], - ["OS=='win'", { - "libraries": [ - "<(module_root_dir)/libs/libsimplex.lib" - ], - "copies": [{ - "destination": "<(PRODUCT_DIR)", - "files": [ - "<(module_root_dir)/libs/*" - ] - }] + "libraries": [ "-ldl" ] }] ] } diff --git a/packages/simplex-chat-nodejs/cpp/simplex.cc b/packages/simplex-chat-nodejs/cpp/simplex.cc index bcca2b994a..6cc2172ee1 100644 --- a/packages/simplex-chat-nodejs/cpp/simplex.cc +++ b/packages/simplex-chat-nodejs/cpp/simplex.cc @@ -8,15 +8,102 @@ #include #include #include +#include #include #include #include +#ifdef _WIN32 +#include +#else +#include +#endif #include "simplex.h" namespace simplex { using namespace Napi; +struct Library { + std::string path; + hs_init_with_rtsopts_fn hs_init_with_rtsopts = nullptr; + hs_thread_done_fn hs_thread_done = nullptr; + chat_migrate_init_fn chat_migrate_init = nullptr; + chat_migrate_init_queue_fn chat_migrate_init_queue = nullptr; + chat_close_store_fn chat_close_store = nullptr; + chat_send_cmd_fn chat_send_cmd = nullptr; + chat_recv_msg_wait_fn chat_recv_msg_wait = nullptr; + chat_write_file_fn chat_write_file = nullptr; + chat_read_file_fn chat_read_file = nullptr; + chat_encrypt_file_fn chat_encrypt_file = nullptr; + chat_decrypt_file_fn chat_decrypt_file = nullptr; +}; + +static Library lib; +static std::atomic loaded{false}; +static std::mutex load_mutex; + +#ifdef _WIN32 +typedef HMODULE LibHandle; + +static LibHandle OpenLibrary(const std::string& path) { + int n = MultiByteToWideChar(CP_UTF8, 0, path.c_str(), -1, nullptr, 0); + std::wstring wpath(n, L'\0'); + MultiByteToWideChar(CP_UTF8, 0, path.c_str(), -1, &wpath[0], n); + // dependencies resolve from the libsimplex.dll, application and system dirs, not the cwd or PATH + return LoadLibraryExW(wpath.c_str(), nullptr, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); +} + +static void CloseLibrary(LibHandle h) { + FreeLibrary(h); +} + +static void* FindSymbol(LibHandle h, const char* name) { + return reinterpret_cast(GetProcAddress(h, name)); +} + +static std::string LastLoadError() { + DWORD code = GetLastError(); + char* msg = nullptr; + FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, code, 0, reinterpret_cast(&msg), 0, nullptr); + std::string err = msg ? msg : "error " + std::to_string(code); + LocalFree(msg); + // system messages end with CRLF + err.erase(err.find_last_not_of("\r\n") + 1); + return err; +} +#else +typedef void* LibHandle; + +static LibHandle OpenLibrary(const std::string& path) { + return dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); +} + +static void CloseLibrary(LibHandle h) { + dlclose(h); +} + +static void* FindSymbol(LibHandle h, const char* name) { + return dlsym(h, name); +} + +static std::string LastLoadError() { + const char* err = dlerror(); + return err ? err : "unknown error"; +} +#endif + +template +static void Resolve(LibHandle h, const char* name, F& fn) { + fn = reinterpret_cast(FindSymbol(h, name)); +} + +template +static void ResolveRequired(LibHandle h, const char* name, F& fn, std::string& missing) { + Resolve(h, name, fn); + if (fn == nullptr && missing.empty()) missing = name; +} + void haskell_init() { #ifdef _WIN32 // non-moving GC is broken on windows with GHC 9.4-9.6.3 @@ -40,7 +127,57 @@ void haskell_init() { nullptr}; #endif char **pargv = const_cast(argv); - hs_init_with_rtsopts(&argc, &pargv); + lib.hs_init_with_rtsopts(&argc, &pargv); +} + +static bool RequireLoaded(Env env) { + if (loaded) return true; + Error::New(env, "libsimplex is not loaded, call core.loadLibrary(backend) first").ThrowAsJavaScriptException(); + return false; +} + +Value Load(const CallbackInfo& args) { + Env env = args.Env(); + if (args.Length() < 1 || !args[0].IsString()) { + TypeError::New(env, "Expected string (libPath)").ThrowAsJavaScriptException(); + return env.Undefined(); + } + std::string path = args[0].As().Utf8Value(); + // worker_threads may load concurrently; the Haskell runtime must be initialized once per process + std::lock_guard lock(load_mutex); + if (loaded) { + if (path != lib.path) Error::New(env, "libsimplex already loaded from " + lib.path).ThrowAsJavaScriptException(); + return env.Undefined(); + } + LibHandle h = OpenLibrary(path); + if (h == nullptr) { + std::string err = LastLoadError(); + Error::New(env, "cannot load " + path + ": " + err).ThrowAsJavaScriptException(); + return env.Undefined(); + } + Library l; + l.path = path; + std::string missing; + ResolveRequired(h, "hs_init_with_rtsopts", l.hs_init_with_rtsopts, missing); + ResolveRequired(h, "chat_migrate_init", l.chat_migrate_init, missing); + ResolveRequired(h, "chat_close_store", l.chat_close_store, missing); + ResolveRequired(h, "chat_send_cmd", l.chat_send_cmd, missing); + ResolveRequired(h, "chat_recv_msg_wait", l.chat_recv_msg_wait, missing); + ResolveRequired(h, "chat_write_file", l.chat_write_file, missing); + ResolveRequired(h, "chat_read_file", l.chat_read_file, missing); + ResolveRequired(h, "chat_encrypt_file", l.chat_encrypt_file, missing); + ResolveRequired(h, "chat_decrypt_file", l.chat_decrypt_file, missing); + if (!missing.empty()) { + CloseLibrary(h); + Error::New(env, path + " does not export " + missing).ThrowAsJavaScriptException(); + return env.Undefined(); + } + Resolve(h, "chat_migrate_init_queue", l.chat_migrate_init_queue); + Resolve(h, "hs_thread_done", l.hs_thread_done); + lib = l; + haskell_init(); + loaded = true; + return env.Undefined(); } class ResultAsyncWorker : public AsyncWorker { @@ -287,7 +424,7 @@ class Receiver { request = std::move(queue_.front()); queue_.pop_front(); } - char* c_res = chat_recv_msg_wait(ctrl_, request.wait); + char* c_res = lib.chat_recv_msg_wait(ctrl_, request.wait); napi_status status = Settle(request.deferred, [c_res](Napi::Env env, Promise::Deferred& deferred) { if (c_res == nullptr) { deferred.Reject(Error::New(env, "chat_recv_msg_wait failed").Value()); @@ -312,7 +449,7 @@ class Receiver { } pending_->Tsfn().Release(); // Each OS thread that enters Haskell keeps an RTS task record until it calls hs_thread_done. - hs_thread_done(); + if (lib.hs_thread_done) lib.hs_thread_done(); } template @@ -361,6 +498,7 @@ ResultAsyncWorker::ResultProcessor MigrateResultProcessor() { Value ChatMigrateInit(const CallbackInfo& args) { Env env = args.Env(); + if (!RequireLoaded(env)) return env.Undefined(); if (args.Length() < 3 || !args[0].IsString() || !args[1].IsString() || !args[2].IsString()) { TypeError::New(env, "Expected three string arguments").ThrowAsJavaScriptException(); return env.Undefined(); @@ -375,7 +513,7 @@ Value ChatMigrateInit(const CallbackInfo& args) { 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); + char* c_res = lib.chat_migrate_init(path.c_str(), key.c_str(), confirm.c_str(), &ctrl); worker->SetCtrl(reinterpret_cast(ctrl)); HandleCResult(worker, c_res, "chat_migrate_init"); }; @@ -388,6 +526,11 @@ Value ChatMigrateInit(const CallbackInfo& args) { Value ChatMigrateInitQueue(const CallbackInfo& args) { Env env = args.Env(); + if (!RequireLoaded(env)) return env.Undefined(); + if (lib.chat_migrate_init_queue == nullptr) { + Error::New(env, "loaded libsimplex does not export chat_migrate_init_queue; queue size needs a newer libsimplex").ThrowAsJavaScriptException(); + return env.Undefined(); + } if (args.Length() < 4 || !args[0].IsString() || !args[1].IsString() || !args[2].IsString() || !args[3].IsNumber()) { TypeError::New(env, "Expected three string arguments and number").ThrowAsJavaScriptException(); return env.Undefined(); @@ -408,7 +551,7 @@ Value ChatMigrateInitQueue(const CallbackInfo& args) { auto execute_fn = [path, key, confirm, queue_size](ResultAsyncWorker* worker) { chat_ctrl ctrl = nullptr; - char* c_res = chat_migrate_init_queue(path.c_str(), key.c_str(), confirm.c_str(), queue_size, &ctrl); + char* c_res = lib.chat_migrate_init_queue(path.c_str(), key.c_str(), confirm.c_str(), queue_size, &ctrl); worker->SetCtrl(reinterpret_cast(ctrl)); HandleCResult(worker, c_res, "chat_migrate_init_queue"); }; @@ -421,6 +564,7 @@ Value ChatMigrateInitQueue(const CallbackInfo& args) { Value ChatCloseStore(const CallbackInfo& args) { Env env = args.Env(); + if (!RequireLoaded(env)) return env.Undefined(); if (args.Length() < 1 || !args[0].IsBigInt()) { TypeError::New(env, "Expected bigint (ctrl)").ThrowAsJavaScriptException(); return env.Undefined(); @@ -436,7 +580,7 @@ Value ChatCloseStore(const CallbackInfo& args) { if (receiver) { receiver->Stop(); } - char* c_res = chat_close_store(ctrl); + char* c_res = lib.chat_close_store(ctrl); HandleCResult(worker, c_res, "chat_close_store"); }; @@ -448,6 +592,7 @@ Value ChatCloseStore(const CallbackInfo& args) { Value ChatSendCmd(const CallbackInfo& args) { Env env = args.Env(); + if (!RequireLoaded(env)) return env.Undefined(); if (args.Length() < 2 || !args[0].IsBigInt() || !args[1].IsString()) { TypeError::New(env, "Expected bigint (ctrl) and string (cmd)").ThrowAsJavaScriptException(); return env.Undefined(); @@ -460,7 +605,7 @@ Value ChatSendCmd(const CallbackInfo& args) { Promise promise = CreatePromiseAndCallback(env, cb); auto execute_fn = [ctrl, cmd](ResultAsyncWorker* worker) { - char* c_res = chat_send_cmd(ctrl, cmd.c_str()); + char* c_res = lib.chat_send_cmd(ctrl, cmd.c_str()); HandleCResult(worker, c_res, "chat_send_cmd"); }; @@ -472,6 +617,7 @@ Value ChatSendCmd(const CallbackInfo& args) { Value ChatRecvMsgWait(const CallbackInfo& args) { Env env = args.Env(); + if (!RequireLoaded(env)) return env.Undefined(); if (args.Length() < 2 || !args[0].IsBigInt() || !args[1].IsNumber()) { TypeError::New(env, "Expected bigint (ctrl), number (wait)").ThrowAsJavaScriptException(); return env.Undefined(); @@ -503,6 +649,7 @@ Value ChatRecvMsgWait(const CallbackInfo& args) { Value ChatWriteFile(const CallbackInfo& args) { Env env = args.Env(); + if (!RequireLoaded(env)) return env.Undefined(); if (args.Length() < 3 || !args[0].IsBigInt() || !args[1].IsString() || !(args[2].IsArrayBuffer() || args[2].IsTypedArray())) { TypeError::New(env, "Expected bigint (ctrl), string (path), ArrayBuffer or Uint8Array").ThrowAsJavaScriptException(); return env.Undefined(); @@ -530,7 +677,7 @@ Value ChatWriteFile(const CallbackInfo& args) { Promise promise = CreatePromiseAndCallback(env, cb); auto execute_fn = [ctrl, path, data, len](ResultAsyncWorker* worker) { - char* c_res = chat_write_file(ctrl, path.c_str(), data, static_cast(len)); + char* c_res = lib.chat_write_file(ctrl, path.c_str(), data, static_cast(len)); HandleCResult(worker, c_res, "chat_write_file"); }; @@ -543,6 +690,7 @@ Value ChatWriteFile(const CallbackInfo& args) { Value ChatReadFile(const CallbackInfo& args) { Env env = args.Env(); + if (!RequireLoaded(env)) return env.Undefined(); 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(); @@ -556,7 +704,7 @@ Value ChatReadFile(const CallbackInfo& args) { 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()); + char* buf = lib.chat_read_file(path.c_str(), key.c_str(), nonce.c_str()); if (buf == nullptr) { worker->SetWorkerError("chat_read_file failed"); return; @@ -586,6 +734,7 @@ Value ChatReadFile(const CallbackInfo& args) { Value ChatEncryptFile(const CallbackInfo& args) { Env env = args.Env(); + if (!RequireLoaded(env)) return env.Undefined(); 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(); @@ -599,7 +748,7 @@ Value ChatEncryptFile(const CallbackInfo& args) { Promise promise = CreatePromiseAndCallback(env, cb); auto execute_fn = [ctrl, fromPath, toPath](ResultAsyncWorker* worker) { - char* c_res = chat_encrypt_file(ctrl, fromPath.c_str(), toPath.c_str()); + char* c_res = lib.chat_encrypt_file(ctrl, fromPath.c_str(), toPath.c_str()); HandleCResult(worker, c_res, "chat_encrypt_file"); }; @@ -611,6 +760,7 @@ Value ChatEncryptFile(const CallbackInfo& args) { Value ChatDecryptFile(const CallbackInfo& args) { Env env = args.Env(); + if (!RequireLoaded(env)) return env.Undefined(); 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(); @@ -625,7 +775,7 @@ Value ChatDecryptFile(const CallbackInfo& args) { Promise promise = CreatePromiseAndCallback(env, cb); 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()); + char* c_res = lib.chat_decrypt_file(fromPath.c_str(), key.c_str(), nonce.c_str(), toPath.c_str()); HandleCResult(worker, c_res, "chat_decrypt_file"); }; @@ -636,7 +786,7 @@ Value ChatDecryptFile(const CallbackInfo& args) { } Object Init(Env env, Object exports) { - haskell_init(); + exports.Set("load", Function::New(env, Load)); auto* receivers = new Receivers(); // Stopping all receivers before joining any bounds teardown by the longest in-flight receive. env.AddCleanupHook([receivers]() { diff --git a/packages/simplex-chat-nodejs/cpp/simplex.h b/packages/simplex-chat-nodejs/cpp/simplex.h index ddc052946a..7c44313295 100644 --- a/packages/simplex-chat-nodejs/cpp/simplex.h +++ b/packages/simplex-chat-nodejs/cpp/simplex.h @@ -9,40 +9,30 @@ #ifndef SimpleX_h #define SimpleX_h -extern "C" void hs_init(int argc, char **argv[]); -extern "C" void hs_init_with_rtsopts(int * argc, char **argv[]); -extern "C" void hs_thread_done(void); - typedef long* chat_ctrl; +typedef void (*hs_init_with_rtsopts_fn)(int *argc, char **argv[]); +typedef void (*hs_thread_done_fn)(void); // the last parameter is used to return the pointer to chat controller -extern "C" char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl); -extern "C" char *chat_migrate_init_queue(const char *path, const char *key, const char *confirm, const int queueSize, chat_ctrl *ctrl); -extern "C" char *chat_close_store(chat_ctrl ctrl); -extern "C" char *chat_reopen_store(chat_ctrl ctrl); -extern "C" char *chat_send_cmd(chat_ctrl ctrl, const char *cmd); -extern "C" char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait); -extern "C" char *chat_parse_markdown(const char *str); -extern "C" char *chat_parse_server(const char *str); -extern "C" char *chat_password_hash(const char *pwd, const char *salt); -extern "C" char *chat_valid_name(const char *name); -extern "C" int chat_json_length(const char *str); -extern "C" char *chat_encrypt_media(chat_ctrl ctrl, const char *key, const char *frame, const int len); -extern "C" char *chat_decrypt_media(const char *key, const char *frame, const int len); +typedef char *(*chat_migrate_init_fn)(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl); +typedef char *(*chat_migrate_init_queue_fn)(const char *path, const char *key, const char *confirm, int queueSize, chat_ctrl *ctrl); +typedef char *(*chat_close_store_fn)(chat_ctrl ctrl); +typedef char *(*chat_send_cmd_fn)(chat_ctrl ctrl, const char *cmd); +typedef char *(*chat_recv_msg_wait_fn)(chat_ctrl ctrl, int wait); // chat_write_file returns null-terminated string with JSON of WriteFileResult -extern "C" char *chat_write_file(chat_ctrl ctrl, const char *path, const char *data, const int len); +typedef char *(*chat_write_file_fn)(chat_ctrl ctrl, const char *path, const char *data, int len); // chat_read_file returns a buffer with: // result status (1 byte), then if // status == 0 (success): buffer length (uint32, 4 bytes), buffer of specified length. // status == 1 (error): null-terminated error message string. -extern "C" char *chat_read_file(const char *path, const char *key, const char *nonce); +typedef char *(*chat_read_file_fn)(const char *path, const char *key, const char *nonce); // chat_encrypt_file returns null-terminated string with JSON of WriteFileResult -extern "C" char *chat_encrypt_file(chat_ctrl ctrl, const char *fromPath, const char *toPath); +typedef char *(*chat_encrypt_file_fn)(chat_ctrl ctrl, const char *fromPath, const char *toPath); // chat_decrypt_file returns null-terminated string with the error message -extern "C" char *chat_decrypt_file(const char *fromPath, const char *key, const char *nonce, const char *toPath); +typedef char *(*chat_decrypt_file_fn)(const char *fromPath, const char *key, const char *nonce, const char *toPath); #endif /* simplex_h */ \ No newline at end of file diff --git a/packages/simplex-chat-nodejs/docs/Namespace.core.md b/packages/simplex-chat-nodejs/docs/Namespace.core.md index 82b0d9ffba..fdf9f8b4dd 100644 --- a/packages/simplex-chat-nodejs/docs/Namespace.core.md +++ b/packages/simplex-chat-nodejs/docs/Namespace.core.md @@ -32,6 +32,7 @@ You are unlikely to ever need to use this module directly. ## Type Aliases +- [Backend](core.TypeAlias.Backend.md) - [DBMigrationError](core.TypeAlias.DBMigrationError.md) - [MigrationError](core.TypeAlias.MigrationError.md) - [MTRError](core.TypeAlias.MTRError.md) @@ -46,3 +47,4 @@ You are unlikely to ever need to use this module directly. - [chatRecvMsgWait](core.Function.chatRecvMsgWait.md) - [chatSendCmd](core.Function.chatSendCmd.md) - [chatWriteFile](core.Function.chatWriteFile.md) +- [loadLibrary](core.Function.loadLibrary.md) diff --git a/packages/simplex-chat-nodejs/docs/api.Class.ChatApi.md b/packages/simplex-chat-nodejs/docs/api.Class.ChatApi.md index 287099906b..cc0e9eec3c 100644 --- a/packages/simplex-chat-nodejs/docs/api.Class.ChatApi.md +++ b/packages/simplex-chat-nodejs/docs/api.Class.ChatApi.md @@ -6,7 +6,7 @@ # Class: ChatApi -Defined in: [src/api.ts:97](../src/api.ts#L97) +Defined in: [src/api.ts:95](../src/api.ts#L95) Main API class for interacting with the chat core library. @@ -16,7 +16,7 @@ Main API class for interacting with the chat core library. > `protected` **ctrl\_**: `bigint` \| `undefined` -Defined in: [src/api.ts:103](../src/api.ts#L103) +Defined in: [src/api.ts:101](../src/api.ts#L101) ## Accessors @@ -26,7 +26,7 @@ Defined in: [src/api.ts:103](../src/api.ts#L103) > **get** **ctrl**(): `bigint` -Defined in: [src/api.ts:344](../src/api.ts#L344) +Defined in: [src/api.ts:343](../src/api.ts#L343) Chat controller reference @@ -42,7 +42,7 @@ Chat controller reference > **get** **initialized**(): `boolean` -Defined in: [src/api.ts:330](../src/api.ts#L330) +Defined in: [src/api.ts:329](../src/api.ts#L329) Chat controller is initialized @@ -58,7 +58,7 @@ Chat controller is initialized > **get** **started**(): `boolean` -Defined in: [src/api.ts:337](../src/api.ts#L337) +Defined in: [src/api.ts:336](../src/api.ts#L336) Chat controller is started @@ -72,7 +72,7 @@ Chat controller is started > **apiAcceptContactRequest**(`contactReqId`): `Promise`\<`Contact`\> -Defined in: [src/api.ts:750](../src/api.ts#L750) +Defined in: [src/api.ts:749](../src/api.ts#L749) Accept contact request. Network usage: interactive. @@ -93,7 +93,7 @@ Network usage: interactive. > **apiAcceptMember**(`groupId`, `groupMemberId`, `memberRole`): `Promise`\<`GroupMember`\> -Defined in: [src/api.ts:570](../src/api.ts#L570) +Defined in: [src/api.ts:569](../src/api.ts#L569) Accept group member. Requires Admin role. Network usage: background. @@ -122,7 +122,7 @@ Network usage: background. > **apiAddMember**(`groupId`, `contactId`, `memberRole`): `Promise`\<`GroupMember`\> -Defined in: [src/api.ts:550](../src/api.ts#L550) +Defined in: [src/api.ts:549](../src/api.ts#L549) Add contact to group. Requires bot to have Admin role. Network usage: interactive. @@ -151,7 +151,7 @@ Network usage: interactive. > **apiBlockMembersForAll**(`groupId`, `groupMemberIds`, `blocked`): `Promise`\<`void`\> -Defined in: [src/api.ts:590](../src/api.ts#L590) +Defined in: [src/api.ts:589](../src/api.ts#L589) Block members. Requires Moderator role. Network usage: background. @@ -180,7 +180,7 @@ Network usage: background. > **apiCancelFile**(`fileId`): `Promise`\<`void`\> -Defined in: [src/api.ts:540](../src/api.ts#L540) +Defined in: [src/api.ts:539](../src/api.ts#L539) Cancel file. Network usage: background. @@ -201,7 +201,7 @@ Network usage: background. > **apiChatItemReaction**(`chatType`, `chatId`, `chatItemId`, `add`, `reaction`): `Promise`\<`ACIReaction`\> -Defined in: [src/api.ts:513](../src/api.ts#L513) +Defined in: [src/api.ts:512](../src/api.ts#L512) Add/remove message reaction. Network usage: background. @@ -238,7 +238,7 @@ Network usage: background. > **apiConnect**(`userId`, `incognito`, `preparedLink`): `Promise`\<[`ConnReqType`](api.Enumeration.ConnReqType.md)\> -Defined in: [src/api.ts:719](../src/api.ts#L719) +Defined in: [src/api.ts:718](../src/api.ts#L718) Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link Network usage: interactive. @@ -267,7 +267,7 @@ Network usage: interactive. > **apiConnectActiveUser**(`connLink`): `Promise`\<[`ConnReqType`](api.Enumeration.ConnReqType.md)\> -Defined in: [src/api.ts:728](../src/api.ts#L728) +Defined in: [src/api.ts:727](../src/api.ts#L727) Connect via SimpleX link as string in the active user profile. Network usage: interactive. @@ -288,7 +288,7 @@ Network usage: interactive. > **apiConnectPlan**(`userId`, `connectionLink`): `Promise`\<\[`ConnectionPlan`, `CreatedConnLink`\]\> -Defined in: [src/api.ts:709](../src/api.ts#L709) +Defined in: [src/api.ts:708](../src/api.ts#L708) Determine SimpleX link type and if the bot is already connected via this link. Network usage: interactive. @@ -313,7 +313,7 @@ Network usage: interactive. > **apiCreateActiveUser**(`profile?`): `Promise`\<`User`\> -Defined in: [src/api.ts:887](../src/api.ts#L887) +Defined in: [src/api.ts:886](../src/api.ts#L886) Create new user profile Network usage: no. @@ -334,7 +334,7 @@ Network usage: no. > **apiCreateGroupLink**(`groupId`, `memberRole`): `Promise`\<`string`\> -Defined in: [src/api.ts:650](../src/api.ts#L650) +Defined in: [src/api.ts:649](../src/api.ts#L649) Create group link. Network usage: interactive. @@ -359,7 +359,7 @@ Network usage: interactive. > **apiCreateLink**(`userId`): `Promise`\<`string`\> -Defined in: [src/api.ts:696](../src/api.ts#L696) +Defined in: [src/api.ts:695](../src/api.ts#L695) Create 1-time invitation link. Network usage: interactive. @@ -380,7 +380,7 @@ Network usage: interactive. > **apiCreateMemberContact**(`groupId`, `groupMemberId`): `Promise`\<`Contact`\> -Defined in: [src/api.ts:953](../src/api.ts#L953) +Defined in: [src/api.ts:952](../src/api.ts#L952) Create a direct message contact with a group member. Returns the created contact. @@ -406,7 +406,7 @@ Network usage: interactive. > **apiCreateUserAddress**(`userId`): `Promise`\<`CreatedConnLink`\> -Defined in: [src/api.ts:361](../src/api.ts#L361) +Defined in: [src/api.ts:360](../src/api.ts#L360) Create bot address. Network usage: interactive. @@ -427,7 +427,7 @@ Network usage: interactive. > **apiDeleteChat**(`chatType`, `chatId`, `deleteMode?`): `Promise`\<`void`\> -Defined in: [src/api.ts:809](../src/api.ts#L809) +Defined in: [src/api.ts:808](../src/api.ts#L808) Delete chat. Network usage: background. @@ -456,7 +456,7 @@ Network usage: background. > **apiDeleteChatItems**(`chatType`, `chatId`, `chatItemIds`, `deleteMode`): `Promise`\<`ChatItemDeletion`[]\> -Defined in: [src/api.ts:488](../src/api.ts#L488) +Defined in: [src/api.ts:487](../src/api.ts#L487) Delete message. Network usage: background. @@ -489,7 +489,7 @@ Network usage: background. > **apiDeleteGroupLink**(`groupId`): `Promise`\<`void`\> -Defined in: [src/api.ts:672](../src/api.ts#L672) +Defined in: [src/api.ts:671](../src/api.ts#L671) Delete group link. Network usage: background. @@ -510,7 +510,7 @@ Network usage: background. > **apiDeleteMemberChatItem**(`groupId`, `chatItemIds`): `Promise`\<`ChatItemDeletion`[]\> -Defined in: [src/api.ts:503](../src/api.ts#L503) +Defined in: [src/api.ts:502](../src/api.ts#L502) Moderate message. Requires Moderator role (and higher than message author's). Network usage: background. @@ -535,7 +535,7 @@ Network usage: background. > **apiDeleteUser**(`userId`, `delSMPQueues`, `viewPwd?`): `Promise`\<`void`\> -Defined in: [src/api.ts:917](../src/api.ts#L917) +Defined in: [src/api.ts:916](../src/api.ts#L916) Delete user profile. Network usage: background. @@ -564,7 +564,7 @@ Network usage: background. > **apiDeleteUserAddress**(`userId`): `Promise`\<`void`\> -Defined in: [src/api.ts:371](../src/api.ts#L371) +Defined in: [src/api.ts:370](../src/api.ts#L370) Deletes a user address. Network usage: background. @@ -585,7 +585,7 @@ Network usage: background. > **apiGetActiveUser**(): `Promise`\<`User` \| `undefined`\> -Defined in: [src/api.ts:867](../src/api.ts#L867) +Defined in: [src/api.ts:866](../src/api.ts#L866) Get active user profile Network usage: no. @@ -600,7 +600,7 @@ Network usage: no. > **apiGetChat**(`chatType`, `chatId`, `count`): `Promise`\<`any`\> -Defined in: [src/api.ts:857](../src/api.ts#L857) +Defined in: [src/api.ts:856](../src/api.ts#L856) Get chat items. Network usage: no. @@ -629,7 +629,7 @@ Network usage: no. > **apiGetChats**(`userId`, `pagination`, `query?`, `pendingConnections?`): `Promise`\<`AChat`[]\> -Defined in: [src/api.ts:794](../src/api.ts#L794) +Defined in: [src/api.ts:793](../src/api.ts#L793) Get chat previews (paginated). Network usage: no. @@ -666,7 +666,7 @@ on large databases. > **apiGetGroupLink**(`groupId`): `Promise`\<`GroupLink`\> -Defined in: [src/api.ts:681](../src/api.ts#L681) +Defined in: [src/api.ts:680](../src/api.ts#L680) Get group link. Network usage: no. @@ -687,7 +687,7 @@ Network usage: no. > **apiGetGroupLinkStr**(`groupId`): `Promise`\<`string`\> -Defined in: [src/api.ts:687](../src/api.ts#L687) +Defined in: [src/api.ts:686](../src/api.ts#L686) #### Parameters @@ -705,7 +705,7 @@ Defined in: [src/api.ts:687](../src/api.ts#L687) > **apiGetUserAddress**(`userId`): `Promise`\<`UserContactLink` \| `undefined`\> -Defined in: [src/api.ts:381](../src/api.ts#L381) +Defined in: [src/api.ts:380](../src/api.ts#L380) Get bot address and settings. Network usage: no. @@ -726,7 +726,7 @@ Network usage: no. > **apiJoinGroup**(`groupId`): `Promise`\<`GroupInfo`\> -Defined in: [src/api.ts:560](../src/api.ts#L560) +Defined in: [src/api.ts:559](../src/api.ts#L559) Join group. Network usage: interactive. @@ -747,7 +747,7 @@ Network usage: interactive. > **apiLeaveGroup**(`groupId`): `Promise`\<`GroupInfo`\> -Defined in: [src/api.ts:610](../src/api.ts#L610) +Defined in: [src/api.ts:609](../src/api.ts#L609) Leave group. Network usage: background. @@ -768,7 +768,7 @@ Network usage: background. > **apiListContacts**(`userId`): `Promise`\<`Contact`[]\> -Defined in: [src/api.ts:770](../src/api.ts#L770) +Defined in: [src/api.ts:769](../src/api.ts#L769) Get contacts. Network usage: no. @@ -789,7 +789,7 @@ Network usage: no. > **apiListGroups**(`userId`, `contactId?`, `search?`): `Promise`\<`GroupInfo`[]\> -Defined in: [src/api.ts:780](../src/api.ts#L780) +Defined in: [src/api.ts:779](../src/api.ts#L779) Get groups. Network usage: no. @@ -818,7 +818,7 @@ Network usage: no. > **apiListMembers**(`groupId`): `Promise`\<`GroupMember`[]\> -Defined in: [src/api.ts:620](../src/api.ts#L620) +Defined in: [src/api.ts:619](../src/api.ts#L619) Get group members. Network usage: no. @@ -839,7 +839,7 @@ Network usage: no. > **apiListUsers**(): `Promise`\<`UserInfo`[]\> -Defined in: [src/api.ts:897](../src/api.ts#L897) +Defined in: [src/api.ts:896](../src/api.ts#L896) Get all user profiles Network usage: no. @@ -854,7 +854,7 @@ Network usage: no. > **apiNewGroup**(`userId`, `groupProfile`): `Promise`\<`GroupInfo`\> -Defined in: [src/api.ts:630](../src/api.ts#L630) +Defined in: [src/api.ts:629](../src/api.ts#L629) Create group. Network usage: no. @@ -879,7 +879,7 @@ Network usage: no. > **apiReceiveFile**(`fileId`): `Promise`\<`AChatItem`\> -Defined in: [src/api.ts:529](../src/api.ts#L529) +Defined in: [src/api.ts:528](../src/api.ts#L528) Receive file. Network usage: no. @@ -900,7 +900,7 @@ Network usage: no. > **apiRejectContactRequest**(`contactReqId`): `Promise`\<`void`\> -Defined in: [src/api.ts:760](../src/api.ts#L760) +Defined in: [src/api.ts:759](../src/api.ts#L759) Reject contact request. The user who sent the request is **not notified**. Network usage: no. @@ -921,7 +921,7 @@ Network usage: no. > **apiRemoveMembers**(`groupId`, `memberIds`, `withMessages?`): `Promise`\<`GroupMember`[]\> -Defined in: [src/api.ts:600](../src/api.ts#L600) +Defined in: [src/api.ts:599](../src/api.ts#L599) Remove members. Requires Admin role. Network usage: background. @@ -950,7 +950,7 @@ Network usage: background. > **apiSendMemberContactInvitation**(`contactId`, `message?`): `Promise`\<`Contact`\> -Defined in: [src/api.ts:964](../src/api.ts#L964) +Defined in: [src/api.ts:963](../src/api.ts#L963) Send a direct message invitation to a group member contact. The contact must have been created with [apiCreateMemberContact](#apicreatemembercontact). @@ -976,7 +976,7 @@ Network usage: interactive. > **apiSendMessages**(`chat`, `messages`, `liveMessage?`): `Promise`\<`AChatItem`[]\> -Defined in: [src/api.ts:432](../src/api.ts#L432) +Defined in: [src/api.ts:431](../src/api.ts#L431) Send messages. Network usage: background. @@ -1005,7 +1005,7 @@ Network usage: background. > **apiSendTextMessage**(`chat`, `text`, `inReplyTo?`): `Promise`\<`AChatItem`[]\> -Defined in: [src/api.ts:455](../src/api.ts#L455) +Defined in: [src/api.ts:454](../src/api.ts#L454) Send text message. Network usage: background. @@ -1034,7 +1034,7 @@ Network usage: background. > **apiSendTextReply**(`chatItem`, `text`): `Promise`\<`AChatItem`[]\> -Defined in: [src/api.ts:463](../src/api.ts#L463) +Defined in: [src/api.ts:462](../src/api.ts#L462) Send text message in reply to received message. Network usage: background. @@ -1059,7 +1059,7 @@ Network usage: background. > **apiSetActiveUser**(`userId`, `viewPwd?`): `Promise`\<`User`\> -Defined in: [src/api.ts:907](../src/api.ts#L907) +Defined in: [src/api.ts:906](../src/api.ts#L906) Set active user profile Network usage: no. @@ -1084,7 +1084,7 @@ Network usage: no. > **apiSetAddressSettings**(`userId`, `__namedParameters`): `Promise`\<`void`\> -Defined in: [src/api.ts:415](../src/api.ts#L415) +Defined in: [src/api.ts:414](../src/api.ts#L414) Set bot address settings. Network usage: interactive. @@ -1109,7 +1109,7 @@ Network usage: interactive. > **apiSetAutoAcceptMemberContacts**(`userId`, `onOff`): `Promise`\<`void`\> -Defined in: [src/api.ts:846](../src/api.ts#L846) +Defined in: [src/api.ts:845](../src/api.ts#L845) Set auto-accept member contacts. Network usage: no. @@ -1134,7 +1134,7 @@ Network usage: no. > **apiSetContactCustomData**(`contactId`, `customData?`): `Promise`\<`void`\> -Defined in: [src/api.ts:836](../src/api.ts#L836) +Defined in: [src/api.ts:835](../src/api.ts#L835) Set contact custom data. Network usage: no. @@ -1159,7 +1159,7 @@ Network usage: no. > **apiSetContactPrefs**(`contactId`, `preferences`): `Promise`\<`void`\> -Defined in: [src/api.ts:943](../src/api.ts#L943) +Defined in: [src/api.ts:942](../src/api.ts#L942) Configure chat preference overrides for the contact. Network usage: background. @@ -1184,7 +1184,7 @@ Network usage: background. > **apiSetGroupCustomData**(`groupId`, `customData?`): `Promise`\<`void`\> -Defined in: [src/api.ts:826](../src/api.ts#L826) +Defined in: [src/api.ts:825](../src/api.ts#L825) Set group custom data. Network usage: no. @@ -1209,7 +1209,7 @@ Network usage: no. > **apiSetGroupLinkMemberRole**(`groupId`, `memberRole`): `Promise`\<`void`\> -Defined in: [src/api.ts:663](../src/api.ts#L663) +Defined in: [src/api.ts:662](../src/api.ts#L662) Set member role for group link. Network usage: no. @@ -1234,7 +1234,7 @@ Network usage: no. > **apiSetMembersRole**(`groupId`, `groupMemberIds`, `memberRole`): `Promise`\<`void`\> -Defined in: [src/api.ts:580](../src/api.ts#L580) +Defined in: [src/api.ts:579](../src/api.ts#L579) Set members role. Requires Admin role. Network usage: background. @@ -1263,7 +1263,7 @@ Network usage: background. > **apiSetProfileAddress**(`userId`, `enable`): `Promise`\<`UserProfileUpdateSummary`\> -Defined in: [src/api.ts:399](../src/api.ts#L399) +Defined in: [src/api.ts:398](../src/api.ts#L398) Add address to bot profile. Network usage: interactive. @@ -1288,7 +1288,7 @@ Network usage: interactive. > **apiUpdateChatItem**(`chatType`, `chatId`, `chatItemId`, `msgContent`, `liveMessage`): `Promise`\<`ChatItem`\> -Defined in: [src/api.ts:471](../src/api.ts#L471) +Defined in: [src/api.ts:470](../src/api.ts#L470) Update message. Network usage: background. @@ -1325,7 +1325,7 @@ Network usage: background. > **apiUpdateGroupProfile**(`groupId`, `groupProfile`): `Promise`\<`GroupInfo`\> -Defined in: [src/api.ts:640](../src/api.ts#L640) +Defined in: [src/api.ts:639](../src/api.ts#L639) Update group profile. Network usage: background. @@ -1350,7 +1350,7 @@ Network usage: background. > **apiUpdateProfile**(`userId`, `profile`): `Promise`\<`UserProfileUpdateSummary` \| `undefined`\> -Defined in: [src/api.ts:927](../src/api.ts#L927) +Defined in: [src/api.ts:926](../src/api.ts#L926) Update user profile. Network usage: background. @@ -1375,7 +1375,7 @@ Network usage: background. > **close**(): `Promise`\<`void`\> -Defined in: [src/api.ts:158](../src/api.ts#L158) +Defined in: [src/api.ts:157](../src/api.ts#L157) Stop chat controller and close chat database. The database is not closed if stopping fails. @@ -1391,7 +1391,7 @@ Usually doesn't need to be called in chat bots. > **off**\<`K`\>(`event`, `subscriber?`): `void` -Defined in: [src/api.ts:302](../src/api.ts#L302) +Defined in: [src/api.ts:301](../src/api.ts#L301) Unsubscribe all or a specific handler from a specific event. @@ -1425,7 +1425,7 @@ An optional subscriber function for the event. > **offAny**(`receiver?`): `void` -Defined in: [src/api.ts:318](../src/api.ts#L318) +Defined in: [src/api.ts:317](../src/api.ts#L317) Unsubscribe all or a specific handler from any events. @@ -1449,7 +1449,7 @@ An optional subscriber function for the event. > **on**\<`K`\>(`subscribers`): `void` -Defined in: [src/api.ts:212](../src/api.ts#L212) +Defined in: [src/api.ts:211](../src/api.ts#L211) Subscribe multiple event handlers at once. @@ -1479,7 +1479,7 @@ If the same function is subscribed to event. > **on**\<`K`\>(`event`, `subscriber`): `void` -Defined in: [src/api.ts:220](../src/api.ts#L220) +Defined in: [src/api.ts:219](../src/api.ts#L219) Subscribe a handler to a specific event. @@ -1517,7 +1517,7 @@ If the same function is subscribed to event. > **onAny**(`receiver`): `void` -Defined in: [src/api.ts:243](../src/api.ts#L243) +Defined in: [src/api.ts:242](../src/api.ts#L242) Subscribe a handler to any event. @@ -1543,7 +1543,7 @@ If the same function is subscribed to event. > **once**\<`K`\>(`event`, `subscriber`): `void` -Defined in: [src/api.ts:254](../src/api.ts#L254) +Defined in: [src/api.ts:253](../src/api.ts#L253) Subscribe a handler to a specific event to be delivered one time. @@ -1581,7 +1581,7 @@ If the same function is subscribed to event. > **recvChatEvent**(`wait?`): `Promise`\<`ChatEvent` \| `undefined`\> -Defined in: [src/api.ts:353](../src/api.ts#L353) +Defined in: [src/api.ts:352](../src/api.ts#L352) #### Parameters @@ -1599,7 +1599,7 @@ Defined in: [src/api.ts:353](../src/api.ts#L353) > **sendChatCmd**(`cmd`): `Promise`\<`ChatResponse`\> -Defined in: [src/api.ts:349](../src/api.ts#L349) +Defined in: [src/api.ts:348](../src/api.ts#L348) #### Parameters @@ -1617,7 +1617,7 @@ Defined in: [src/api.ts:349](../src/api.ts#L349) > **startChat**(): `Promise`\<`void`\> -Defined in: [src/api.ts:124](../src/api.ts#L124) +Defined in: [src/api.ts:123](../src/api.ts#L123) Start chat controller. Must be called with the existing user profile. @@ -1631,7 +1631,7 @@ Start chat controller. Must be called with the existing user profile. > **stopChat**(): `Promise`\<`void`\> -Defined in: [src/api.ts:147](../src/api.ts#L147) +Defined in: [src/api.ts:146](../src/api.ts#L146) Stop chat controller. `close` calls it before closing the database. @@ -1649,7 +1649,7 @@ Usually doesn't need to be called in chat bots. > **wait**\<`K`\>(`event`): `Promise`\<`ChatEvent` & `object`\> -Defined in: [src/api.ts:262](../src/api.ts#L262) +Defined in: [src/api.ts:261](../src/api.ts#L261) Waits for specific event, with an optional predicate. Returns `undefined` on timeout if specified. @@ -1674,7 +1674,7 @@ Returns `undefined` on timeout if specified. > **wait**\<`K`\>(`event`, `predicate`): `Promise`\<`ChatEvent` & `object`\> -Defined in: [src/api.ts:263](../src/api.ts#L263) +Defined in: [src/api.ts:262](../src/api.ts#L262) Waits for specific event, with an optional predicate. Returns `undefined` on timeout if specified. @@ -1703,7 +1703,7 @@ Returns `undefined` on timeout if specified. > **wait**\<`K`\>(`event`, `timeout`): `Promise`\ -Defined in: [src/api.ts:264](../src/api.ts#L264) +Defined in: [src/api.ts:263](../src/api.ts#L263) Waits for specific event, with an optional predicate. Returns `undefined` on timeout if specified. @@ -1732,7 +1732,7 @@ Returns `undefined` on timeout if specified. > **wait**\<`K`\>(`event`, `predicate`, `timeout`): `Promise`\ -Defined in: [src/api.ts:265](../src/api.ts#L265) +Defined in: [src/api.ts:264](../src/api.ts#L264) Waits for specific event, with an optional predicate. Returns `undefined` on timeout if specified. @@ -1767,9 +1767,9 @@ Returns `undefined` on timeout if specified. > `static` **init**(`db`, `confirm?`, `queueSize?`): `Promise`\<`ChatApi`\> -Defined in: [src/api.ts:111](../src/api.ts#L111) +Defined in: [src/api.ts:109](../src/api.ts#L109) -Initializes the ChatApi. +Initializes the ChatApi, loading libsimplex for `db.type` (downloaded on first use). #### Parameters diff --git a/packages/simplex-chat-nodejs/docs/api.TypeAlias.DbConfig.md b/packages/simplex-chat-nodejs/docs/api.TypeAlias.DbConfig.md index 7fe255327c..26934a9c6b 100644 --- a/packages/simplex-chat-nodejs/docs/api.TypeAlias.DbConfig.md +++ b/packages/simplex-chat-nodejs/docs/api.TypeAlias.DbConfig.md @@ -8,12 +8,10 @@ > **DbConfig** = \{ `encryptionKey?`: `string`; `filePrefix`: `string`; `type`: `"sqlite"`; \} \| \{ `connectionString`: `string`; `schemaPrefix?`: `string`; `type`: `"postgres"`; \} -Defined in: [src/api.ts:65](../src/api.ts#L65) +Defined in: [src/api.ts:63](../src/api.ts#L63) -Database configuration. The native library is built against exactly one -backend (see `simplex_backend` / `SIMPLEX_BACKEND` at install time); this -type makes the caller state which one they are targeting so field names -can't lie about their meaning. +Database configuration. `type` selects the libsimplex backend loaded by +`ChatApi.init`; one backend per process. ## Union Members diff --git a/packages/simplex-chat-nodejs/docs/core.Class.ChatAPIError.md b/packages/simplex-chat-nodejs/docs/core.Class.ChatAPIError.md index 3953d777da..690898c69a 100644 --- a/packages/simplex-chat-nodejs/docs/core.Class.ChatAPIError.md +++ b/packages/simplex-chat-nodejs/docs/core.Class.ChatAPIError.md @@ -6,7 +6,7 @@ # Class: ChatAPIError -Defined in: [src/core.ts:95](../src/core.ts#L95) +Defined in: [src/core.ts:116](../src/core.ts#L116) ## Extends @@ -18,7 +18,7 @@ Defined in: [src/core.ts:95](../src/core.ts#L95) > **new ChatAPIError**(`message`, `chatError?`): `ChatAPIError` -Defined in: [src/core.ts:96](../src/core.ts#L96) +Defined in: [src/core.ts:117](../src/core.ts#L117) #### Parameters @@ -44,7 +44,7 @@ Defined in: [src/core.ts:96](../src/core.ts#L96) > **chatError**: `ChatError` \| `undefined` = `undefined` -Defined in: [src/core.ts:96](../src/core.ts#L96) +Defined in: [src/core.ts:117](../src/core.ts#L117) *** @@ -52,7 +52,7 @@ Defined in: [src/core.ts:96](../src/core.ts#L96) > **message**: `string` -Defined in: [src/core.ts:96](../src/core.ts#L96) +Defined in: [src/core.ts:117](../src/core.ts#L117) #### Inherited from diff --git a/packages/simplex-chat-nodejs/docs/core.Class.ChatInitError.md b/packages/simplex-chat-nodejs/docs/core.Class.ChatInitError.md index 649090e3ac..bf62db5cc4 100644 --- a/packages/simplex-chat-nodejs/docs/core.Class.ChatInitError.md +++ b/packages/simplex-chat-nodejs/docs/core.Class.ChatInitError.md @@ -6,7 +6,7 @@ # Class: ChatInitError -Defined in: [src/core.ts:119](../src/core.ts#L119) +Defined in: [src/core.ts:140](../src/core.ts#L140) ## Extends @@ -18,7 +18,7 @@ Defined in: [src/core.ts:119](../src/core.ts#L119) > **new ChatInitError**(`message`, `dbMigrationError`): `ChatInitError` -Defined in: [src/core.ts:120](../src/core.ts#L120) +Defined in: [src/core.ts:141](../src/core.ts#L141) #### Parameters @@ -44,7 +44,7 @@ Defined in: [src/core.ts:120](../src/core.ts#L120) > **dbMigrationError**: [`DBMigrationError`](core.TypeAlias.DBMigrationError.md) -Defined in: [src/core.ts:120](../src/core.ts#L120) +Defined in: [src/core.ts:141](../src/core.ts#L141) *** @@ -52,7 +52,7 @@ Defined in: [src/core.ts:120](../src/core.ts#L120) > **message**: `string` -Defined in: [src/core.ts:120](../src/core.ts#L120) +Defined in: [src/core.ts:141](../src/core.ts#L141) #### Inherited from diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorMigration.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorMigration.md index fea7bfb531..7bc93312bf 100644 --- a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorMigration.md +++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorMigration.md @@ -6,7 +6,7 @@ # Interface: ErrorMigration -Defined in: [src/core.ts:152](../src/core.ts#L152) +Defined in: [src/core.ts:173](../src/core.ts#L173) ## Extends @@ -18,7 +18,7 @@ Defined in: [src/core.ts:152](../src/core.ts#L152) > **dbFile**: `string` -Defined in: [src/core.ts:154](../src/core.ts#L154) +Defined in: [src/core.ts:175](../src/core.ts#L175) *** @@ -26,7 +26,7 @@ Defined in: [src/core.ts:154](../src/core.ts#L154) > **migrationError**: [`MigrationError`](core.TypeAlias.MigrationError.md) -Defined in: [src/core.ts:155](../src/core.ts#L155) +Defined in: [src/core.ts:176](../src/core.ts#L176) *** @@ -34,7 +34,7 @@ Defined in: [src/core.ts:155](../src/core.ts#L155) > **type**: `"errorMigration"` -Defined in: [src/core.ts:153](../src/core.ts#L153) +Defined in: [src/core.ts:174](../src/core.ts#L174) #### Overrides diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorNotADatabase.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorNotADatabase.md index eb0bfb11cb..dfe51b323d 100644 --- a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorNotADatabase.md +++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorNotADatabase.md @@ -6,7 +6,7 @@ # Interface: ErrorNotADatabase -Defined in: [src/core.ts:147](../src/core.ts#L147) +Defined in: [src/core.ts:168](../src/core.ts#L168) ## Extends @@ -18,7 +18,7 @@ Defined in: [src/core.ts:147](../src/core.ts#L147) > **dbFile**: `string` -Defined in: [src/core.ts:149](../src/core.ts#L149) +Defined in: [src/core.ts:170](../src/core.ts#L170) *** @@ -26,7 +26,7 @@ Defined in: [src/core.ts:149](../src/core.ts#L149) > **type**: `"errorNotADatabase"` -Defined in: [src/core.ts:148](../src/core.ts#L148) +Defined in: [src/core.ts:169](../src/core.ts#L169) #### Overrides diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorSQL.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorSQL.md index ffaa364f9b..0e6a987f27 100644 --- a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorSQL.md +++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorSQL.md @@ -6,7 +6,7 @@ # Interface: ErrorSQL -Defined in: [src/core.ts:158](../src/core.ts#L158) +Defined in: [src/core.ts:179](../src/core.ts#L179) ## Extends @@ -18,7 +18,7 @@ Defined in: [src/core.ts:158](../src/core.ts#L158) > **dbFile**: `string` -Defined in: [src/core.ts:160](../src/core.ts#L160) +Defined in: [src/core.ts:181](../src/core.ts#L181) *** @@ -26,7 +26,7 @@ Defined in: [src/core.ts:160](../src/core.ts#L160) > **migrationSQLError**: `string` -Defined in: [src/core.ts:161](../src/core.ts#L161) +Defined in: [src/core.ts:182](../src/core.ts#L182) *** @@ -34,7 +34,7 @@ Defined in: [src/core.ts:161](../src/core.ts#L161) > **type**: `"errorSQL"` -Defined in: [src/core.ts:159](../src/core.ts#L159) +Defined in: [src/core.ts:180](../src/core.ts#L180) #### Overrides diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.InvalidConfirmation.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.InvalidConfirmation.md index 67711030da..331c54b978 100644 --- a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.InvalidConfirmation.md +++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.InvalidConfirmation.md @@ -6,7 +6,7 @@ # Interface: InvalidConfirmation -Defined in: [src/core.ts:139](../src/core.ts#L139) +Defined in: [src/core.ts:160](../src/core.ts#L160) ## Extends @@ -18,7 +18,7 @@ Defined in: [src/core.ts:139](../src/core.ts#L139) > **type**: `"invalidConfirmation"` -Defined in: [src/core.ts:140](../src/core.ts#L140) +Defined in: [src/core.ts:161](../src/core.ts#L161) #### Overrides diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.InvalidQueueSize.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.InvalidQueueSize.md index 9093b5e9b1..a4bc17e92a 100644 --- a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.InvalidQueueSize.md +++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.InvalidQueueSize.md @@ -6,7 +6,7 @@ # Interface: InvalidQueueSize -Defined in: [src/core.ts:143](../src/core.ts#L143) +Defined in: [src/core.ts:164](../src/core.ts#L164) ## Extends @@ -18,7 +18,7 @@ Defined in: [src/core.ts:143](../src/core.ts#L143) > **type**: `"invalidQueueSize"` -Defined in: [src/core.ts:144](../src/core.ts#L144) +Defined in: [src/core.ts:165](../src/core.ts#L165) #### Overrides diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.TypeAlias.Tag.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.TypeAlias.Tag.md index b04f2f5af1..590625f42d 100644 --- a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.TypeAlias.Tag.md +++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.TypeAlias.Tag.md @@ -8,4 +8,4 @@ > **Tag** = `"invalidConfirmation"` \| `"invalidQueueSize"` \| `"errorNotADatabase"` \| `"errorMigration"` \| `"errorSQL"` -Defined in: [src/core.ts:133](../src/core.ts#L133) +Defined in: [src/core.ts:154](../src/core.ts#L154) diff --git a/packages/simplex-chat-nodejs/docs/core.Enumeration.MigrationConfirmation.md b/packages/simplex-chat-nodejs/docs/core.Enumeration.MigrationConfirmation.md index 48e250c7d4..a5a4897aed 100644 --- a/packages/simplex-chat-nodejs/docs/core.Enumeration.MigrationConfirmation.md +++ b/packages/simplex-chat-nodejs/docs/core.Enumeration.MigrationConfirmation.md @@ -6,7 +6,7 @@ # Enumeration: MigrationConfirmation -Defined in: [src/core.ts:104](../src/core.ts#L104) +Defined in: [src/core.ts:125](../src/core.ts#L125) Migration confirmation mode @@ -16,7 +16,7 @@ Migration confirmation mode > **Console**: `"console"` -Defined in: [src/core.ts:107](../src/core.ts#L107) +Defined in: [src/core.ts:128](../src/core.ts#L128) *** @@ -24,7 +24,7 @@ Defined in: [src/core.ts:107](../src/core.ts#L107) > **Error**: `"error"` -Defined in: [src/core.ts:108](../src/core.ts#L108) +Defined in: [src/core.ts:129](../src/core.ts#L129) *** @@ -32,7 +32,7 @@ Defined in: [src/core.ts:108](../src/core.ts#L108) > **YesUp**: `"yesUp"` -Defined in: [src/core.ts:105](../src/core.ts#L105) +Defined in: [src/core.ts:126](../src/core.ts#L126) *** @@ -40,4 +40,4 @@ Defined in: [src/core.ts:105](../src/core.ts#L105) > **YesUpDown**: `"yesUpDown"` -Defined in: [src/core.ts:106](../src/core.ts#L106) +Defined in: [src/core.ts:127](../src/core.ts#L127) diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatCloseStore.md b/packages/simplex-chat-nodejs/docs/core.Function.chatCloseStore.md index 4b0324b92c..f8a10c4276 100644 --- a/packages/simplex-chat-nodejs/docs/core.Function.chatCloseStore.md +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatCloseStore.md @@ -8,7 +8,7 @@ > **chatCloseStore**(`ctrl`): `Promise`\<`void`\> -Defined in: [src/core.ts:20](../src/core.ts#L20) +Defined in: [src/core.ts:41](../src/core.ts#L41) Close chat store diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatDecryptFile.md b/packages/simplex-chat-nodejs/docs/core.Function.chatDecryptFile.md index b6786fe1cb..4925e29dd2 100644 --- a/packages/simplex-chat-nodejs/docs/core.Function.chatDecryptFile.md +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatDecryptFile.md @@ -8,7 +8,7 @@ > **chatDecryptFile**(`fromPath`, `__namedParameters`, `toPath`): `Promise`\<`void`\> -Defined in: [src/core.ts:76](../src/core.ts#L76) +Defined in: [src/core.ts:97](../src/core.ts#L97) Decrypt file diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatEncryptFile.md b/packages/simplex-chat-nodejs/docs/core.Function.chatEncryptFile.md index 9e30e55a23..79c2ddab26 100644 --- a/packages/simplex-chat-nodejs/docs/core.Function.chatEncryptFile.md +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatEncryptFile.md @@ -8,7 +8,7 @@ > **chatEncryptFile**(`ctrl`, `fromPath`, `toPath`): `Promise`\<[`CryptoArgs`](core.Interface.CryptoArgs.md)\> -Defined in: [src/core.ts:68](../src/core.ts#L68) +Defined in: [src/core.ts:89](../src/core.ts#L89) Encrypt file diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatMigrateInit.md b/packages/simplex-chat-nodejs/docs/core.Function.chatMigrateInit.md index dbe4912520..30c73c5c37 100644 --- a/packages/simplex-chat-nodejs/docs/core.Function.chatMigrateInit.md +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatMigrateInit.md @@ -8,7 +8,7 @@ > **chatMigrateInit**(`dbPath`, `dbKey`, `confirm`, `queueSize?`): `Promise`\<`bigint`\> -Defined in: [src/core.ts:8](../src/core.ts#L8) +Defined in: [src/core.ts:29](../src/core.ts#L29) Initialize chat controller diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatReadFile.md b/packages/simplex-chat-nodejs/docs/core.Function.chatReadFile.md index f6713f22d8..6c88d336b8 100644 --- a/packages/simplex-chat-nodejs/docs/core.Function.chatReadFile.md +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatReadFile.md @@ -8,7 +8,7 @@ > **chatReadFile**(`path`, `__namedParameters`): `Promise`\<`Buffer`\<`ArrayBufferLike`\>\> -Defined in: [src/core.ts:61](../src/core.ts#L61) +Defined in: [src/core.ts:82](../src/core.ts#L82) Read buffer from encrypted file diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatRecvMsgWait.md b/packages/simplex-chat-nodejs/docs/core.Function.chatRecvMsgWait.md index 719cf610a1..672e5390a3 100644 --- a/packages/simplex-chat-nodejs/docs/core.Function.chatRecvMsgWait.md +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatRecvMsgWait.md @@ -8,7 +8,7 @@ > **chatRecvMsgWait**(`ctrl`, `wait`): `Promise`\<`ChatEvent` \| `undefined`\> -Defined in: [src/core.ts:40](../src/core.ts#L40) +Defined in: [src/core.ts:61](../src/core.ts#L61) Receive chat event diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatSendCmd.md b/packages/simplex-chat-nodejs/docs/core.Function.chatSendCmd.md index 4296a714cf..1416eaaec1 100644 --- a/packages/simplex-chat-nodejs/docs/core.Function.chatSendCmd.md +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatSendCmd.md @@ -8,7 +8,7 @@ > **chatSendCmd**(`ctrl`, `cmd`): `Promise`\<`ChatResponse`\> -Defined in: [src/core.ts:28](../src/core.ts#L28) +Defined in: [src/core.ts:49](../src/core.ts#L49) Send chat command as string diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatWriteFile.md b/packages/simplex-chat-nodejs/docs/core.Function.chatWriteFile.md index 4ca640d8c4..905f4df0d0 100644 --- a/packages/simplex-chat-nodejs/docs/core.Function.chatWriteFile.md +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatWriteFile.md @@ -8,7 +8,7 @@ > **chatWriteFile**(`ctrl`, `path`, `buffer`): `Promise`\<[`CryptoArgs`](core.Interface.CryptoArgs.md)\> -Defined in: [src/core.ts:53](../src/core.ts#L53) +Defined in: [src/core.ts:74](../src/core.ts#L74) Write buffer to encrypted file diff --git a/packages/simplex-chat-nodejs/docs/core.Function.loadLibrary.md b/packages/simplex-chat-nodejs/docs/core.Function.loadLibrary.md new file mode 100644 index 0000000000..a2034b953d --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Function.loadLibrary.md @@ -0,0 +1,24 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / loadLibrary + +# Function: loadLibrary() + +> **loadLibrary**(`backend`): `Promise`\<`void`\> + +Defined in: [src/core.ts:13](../src/core.ts#L13) + +Resolve (downloading on first use) and load libsimplex for the backend. +One libsimplex per process: the Haskell runtime is initialized once, and another backend is rejected. + +## Parameters + +### backend + +[`Backend`](core.TypeAlias.Backend.md) + +## Returns + +`Promise`\<`void`\> diff --git a/packages/simplex-chat-nodejs/docs/core.Interface.APIResult.md b/packages/simplex-chat-nodejs/docs/core.Interface.APIResult.md index 2ede6d9ffb..2079e9f4a5 100644 --- a/packages/simplex-chat-nodejs/docs/core.Interface.APIResult.md +++ b/packages/simplex-chat-nodejs/docs/core.Interface.APIResult.md @@ -6,7 +6,7 @@ # Interface: APIResult\ -Defined in: [src/core.ts:90](../src/core.ts#L90) +Defined in: [src/core.ts:111](../src/core.ts#L111) ## Type Parameters @@ -20,7 +20,7 @@ Defined in: [src/core.ts:90](../src/core.ts#L90) > `optional` **error?**: `ChatError` -Defined in: [src/core.ts:92](../src/core.ts#L92) +Defined in: [src/core.ts:113](../src/core.ts#L113) *** @@ -28,4 +28,4 @@ Defined in: [src/core.ts:92](../src/core.ts#L92) > `optional` **result?**: `R` -Defined in: [src/core.ts:91](../src/core.ts#L91) +Defined in: [src/core.ts:112](../src/core.ts#L112) diff --git a/packages/simplex-chat-nodejs/docs/core.Interface.CryptoArgs.md b/packages/simplex-chat-nodejs/docs/core.Interface.CryptoArgs.md index 51a55b86b6..29e29b416b 100644 --- a/packages/simplex-chat-nodejs/docs/core.Interface.CryptoArgs.md +++ b/packages/simplex-chat-nodejs/docs/core.Interface.CryptoArgs.md @@ -6,7 +6,7 @@ # Interface: CryptoArgs -Defined in: [src/core.ts:114](../src/core.ts#L114) +Defined in: [src/core.ts:135](../src/core.ts#L135) File encryption key and nonce @@ -16,7 +16,7 @@ File encryption key and nonce > **fileKey**: `string` -Defined in: [src/core.ts:115](../src/core.ts#L115) +Defined in: [src/core.ts:136](../src/core.ts#L136) *** @@ -24,4 +24,4 @@ Defined in: [src/core.ts:115](../src/core.ts#L115) > **fileNonce**: `string` -Defined in: [src/core.ts:116](../src/core.ts#L116) +Defined in: [src/core.ts:137](../src/core.ts#L137) diff --git a/packages/simplex-chat-nodejs/docs/core.Interface.UpMigration.md b/packages/simplex-chat-nodejs/docs/core.Interface.UpMigration.md index 9e1fca49b4..d7ef4e236b 100644 --- a/packages/simplex-chat-nodejs/docs/core.Interface.UpMigration.md +++ b/packages/simplex-chat-nodejs/docs/core.Interface.UpMigration.md @@ -6,7 +6,7 @@ # Interface: UpMigration -Defined in: [src/core.ts:193](../src/core.ts#L193) +Defined in: [src/core.ts:214](../src/core.ts#L214) ## Properties @@ -14,7 +14,7 @@ Defined in: [src/core.ts:193](../src/core.ts#L193) > **upName**: `string` -Defined in: [src/core.ts:194](../src/core.ts#L194) +Defined in: [src/core.ts:215](../src/core.ts#L215) *** @@ -22,4 +22,4 @@ Defined in: [src/core.ts:194](../src/core.ts#L194) > **withDown**: `boolean` -Defined in: [src/core.ts:195](../src/core.ts#L195) +Defined in: [src/core.ts:216](../src/core.ts#L216) diff --git a/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTREDifferent.md b/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTREDifferent.md index f85fb492be..2acfea33d6 100644 --- a/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTREDifferent.md +++ b/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTREDifferent.md @@ -6,7 +6,7 @@ # Interface: MTREDifferent -Defined in: [src/core.ts:214](../src/core.ts#L214) +Defined in: [src/core.ts:235](../src/core.ts#L235) ## Extends @@ -18,7 +18,7 @@ Defined in: [src/core.ts:214](../src/core.ts#L214) > **downMigrations**: `string`[] -Defined in: [src/core.ts:216](../src/core.ts#L216) +Defined in: [src/core.ts:237](../src/core.ts#L237) *** @@ -26,7 +26,7 @@ Defined in: [src/core.ts:216](../src/core.ts#L216) > **type**: `"different"` -Defined in: [src/core.ts:215](../src/core.ts#L215) +Defined in: [src/core.ts:236](../src/core.ts#L236) #### Overrides diff --git a/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTRENoDown.md b/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTRENoDown.md index f031ac8982..2c614c7662 100644 --- a/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTRENoDown.md +++ b/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTRENoDown.md @@ -6,7 +6,7 @@ # Interface: MTRENoDown -Defined in: [src/core.ts:209](../src/core.ts#L209) +Defined in: [src/core.ts:230](../src/core.ts#L230) ## Extends @@ -18,7 +18,7 @@ Defined in: [src/core.ts:209](../src/core.ts#L209) > **dbMigrations**: `string`[] -Defined in: [src/core.ts:211](../src/core.ts#L211) +Defined in: [src/core.ts:232](../src/core.ts#L232) *** @@ -26,7 +26,7 @@ Defined in: [src/core.ts:211](../src/core.ts#L211) > **type**: `"noDown"` -Defined in: [src/core.ts:210](../src/core.ts#L210) +Defined in: [src/core.ts:231](../src/core.ts#L231) #### Overrides diff --git a/packages/simplex-chat-nodejs/docs/core.MTRError.TypeAlias.Tag.md b/packages/simplex-chat-nodejs/docs/core.MTRError.TypeAlias.Tag.md index 59501b539a..a818dcb97a 100644 --- a/packages/simplex-chat-nodejs/docs/core.MTRError.TypeAlias.Tag.md +++ b/packages/simplex-chat-nodejs/docs/core.MTRError.TypeAlias.Tag.md @@ -8,4 +8,4 @@ > **Tag** = `"noDown"` \| `"different"` -Defined in: [src/core.ts:203](../src/core.ts#L203) +Defined in: [src/core.ts:224](../src/core.ts#L224) diff --git a/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEDowngrade.md b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEDowngrade.md index f6202b9456..dbc4ce2363 100644 --- a/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEDowngrade.md +++ b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEDowngrade.md @@ -6,7 +6,7 @@ # Interface: MEDowngrade -Defined in: [src/core.ts:182](../src/core.ts#L182) +Defined in: [src/core.ts:203](../src/core.ts#L203) ## Extends @@ -18,7 +18,7 @@ Defined in: [src/core.ts:182](../src/core.ts#L182) > **downMigrations**: `string`[] -Defined in: [src/core.ts:184](../src/core.ts#L184) +Defined in: [src/core.ts:205](../src/core.ts#L205) *** @@ -26,7 +26,7 @@ Defined in: [src/core.ts:184](../src/core.ts#L184) > **type**: `"downgrade"` -Defined in: [src/core.ts:183](../src/core.ts#L183) +Defined in: [src/core.ts:204](../src/core.ts#L204) #### Overrides diff --git a/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEUpgrade.md b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEUpgrade.md index 7467895b74..e73c0421f8 100644 --- a/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEUpgrade.md +++ b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEUpgrade.md @@ -6,7 +6,7 @@ # Interface: MEUpgrade -Defined in: [src/core.ts:177](../src/core.ts#L177) +Defined in: [src/core.ts:198](../src/core.ts#L198) ## Extends @@ -18,7 +18,7 @@ Defined in: [src/core.ts:177](../src/core.ts#L177) > **type**: `"upgrade"` -Defined in: [src/core.ts:178](../src/core.ts#L178) +Defined in: [src/core.ts:199](../src/core.ts#L199) #### Overrides @@ -30,4 +30,4 @@ Defined in: [src/core.ts:178](../src/core.ts#L178) > **upMigrations**: [`UpMigration`](core.Interface.UpMigration.md)[] -Defined in: [src/core.ts:179](../src/core.ts#L179) +Defined in: [src/core.ts:200](../src/core.ts#L200) diff --git a/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MigrationError.md b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MigrationError.md index 7b2546fcd5..f8effcbce9 100644 --- a/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MigrationError.md +++ b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MigrationError.md @@ -6,7 +6,7 @@ # Interface: MigrationError -Defined in: [src/core.ts:187](../src/core.ts#L187) +Defined in: [src/core.ts:208](../src/core.ts#L208) ## Extends @@ -18,7 +18,7 @@ Defined in: [src/core.ts:187](../src/core.ts#L187) > **mtrError**: [`MTRError`](core.TypeAlias.MTRError.md) -Defined in: [src/core.ts:189](../src/core.ts#L189) +Defined in: [src/core.ts:210](../src/core.ts#L210) *** @@ -26,7 +26,7 @@ Defined in: [src/core.ts:189](../src/core.ts#L189) > **type**: `"migrationError"` -Defined in: [src/core.ts:188](../src/core.ts#L188) +Defined in: [src/core.ts:209](../src/core.ts#L209) #### Overrides diff --git a/packages/simplex-chat-nodejs/docs/core.MigrationError.TypeAlias.Tag.md b/packages/simplex-chat-nodejs/docs/core.MigrationError.TypeAlias.Tag.md index e2e1dcb33f..f434bbfb12 100644 --- a/packages/simplex-chat-nodejs/docs/core.MigrationError.TypeAlias.Tag.md +++ b/packages/simplex-chat-nodejs/docs/core.MigrationError.TypeAlias.Tag.md @@ -8,4 +8,4 @@ > **Tag** = `"upgrade"` \| `"downgrade"` \| `"migrationError"` -Defined in: [src/core.ts:171](../src/core.ts#L171) +Defined in: [src/core.ts:192](../src/core.ts#L192) diff --git a/packages/simplex-chat-nodejs/docs/core.TypeAlias.Backend.md b/packages/simplex-chat-nodejs/docs/core.TypeAlias.Backend.md new file mode 100644 index 0000000000..9b32c20d5b --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.TypeAlias.Backend.md @@ -0,0 +1,11 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / Backend + +# Type Alias: Backend + +> **Backend** = `"sqlite"` \| `"postgres"` + +Defined in: [src/core.ts:5](../src/core.ts#L5) diff --git a/packages/simplex-chat-nodejs/docs/core.TypeAlias.DBMigrationError.md b/packages/simplex-chat-nodejs/docs/core.TypeAlias.DBMigrationError.md index 3f4797fb8e..9394337fe0 100644 --- a/packages/simplex-chat-nodejs/docs/core.TypeAlias.DBMigrationError.md +++ b/packages/simplex-chat-nodejs/docs/core.TypeAlias.DBMigrationError.md @@ -8,4 +8,4 @@ > **DBMigrationError** = [`InvalidConfirmation`](core.DBMigrationError.Interface.InvalidConfirmation.md) \| [`InvalidQueueSize`](core.DBMigrationError.Interface.InvalidQueueSize.md) \| [`ErrorNotADatabase`](core.DBMigrationError.Interface.ErrorNotADatabase.md) \| [`ErrorMigration`](core.DBMigrationError.Interface.ErrorMigration.md) \| [`ErrorSQL`](core.DBMigrationError.Interface.ErrorSQL.md) -Defined in: [src/core.ts:125](../src/core.ts#L125) +Defined in: [src/core.ts:146](../src/core.ts#L146) diff --git a/packages/simplex-chat-nodejs/docs/core.TypeAlias.MTRError.md b/packages/simplex-chat-nodejs/docs/core.TypeAlias.MTRError.md index 66d6a092dc..447c32eaef 100644 --- a/packages/simplex-chat-nodejs/docs/core.TypeAlias.MTRError.md +++ b/packages/simplex-chat-nodejs/docs/core.TypeAlias.MTRError.md @@ -8,4 +8,4 @@ > **MTRError** = [`MTRENoDown`](core.MTRError.Interface.MTRENoDown.md) \| [`MTREDifferent`](core.MTRError.Interface.MTREDifferent.md) -Defined in: [src/core.ts:198](../src/core.ts#L198) +Defined in: [src/core.ts:219](../src/core.ts#L219) diff --git a/packages/simplex-chat-nodejs/docs/core.TypeAlias.MigrationError.md b/packages/simplex-chat-nodejs/docs/core.TypeAlias.MigrationError.md index 6c0ba77541..b59c335af3 100644 --- a/packages/simplex-chat-nodejs/docs/core.TypeAlias.MigrationError.md +++ b/packages/simplex-chat-nodejs/docs/core.TypeAlias.MigrationError.md @@ -8,4 +8,4 @@ > **MigrationError** = [`MEUpgrade`](core.MigrationError.Interface.MEUpgrade.md) \| [`MEDowngrade`](core.MigrationError.Interface.MEDowngrade.md) \| [`MigrationError`](core.MigrationError.Interface.MigrationError.md) -Defined in: [src/core.ts:165](../src/core.ts#L165) +Defined in: [src/core.ts:186](../src/core.ts#L186) diff --git a/packages/simplex-chat-nodejs/package.json b/packages/simplex-chat-nodejs/package.json index 96012d9457..43e85fb0f7 100644 --- a/packages/simplex-chat-nodejs/package.json +++ b/packages/simplex-chat-nodejs/package.json @@ -16,10 +16,9 @@ "examples" ], "scripts": { - "preinstall": "node src/download-libs.js", "install": "node-gyp configure; node-gyp rebuild --release", "install-tools": "npm install -g node-gyp", - "configure": "node-gyp configure; mkdir libs 2> /dev/null | true", + "configure": "node-gyp configure", "build": "node-gyp rebuild && tsc && cp ./src/simplex.* ./dist", "run": "node src/index.js", "build-run": "node-gyp build && node src/index.js", diff --git a/packages/simplex-chat-nodejs/src/api.ts b/packages/simplex-chat-nodejs/src/api.ts index 8c11e9fde3..b232f32aef 100644 --- a/packages/simplex-chat-nodejs/src/api.ts +++ b/packages/simplex-chat-nodejs/src/api.ts @@ -57,10 +57,8 @@ interface EventSubscriber { } /** - * Database configuration. The native library is built against exactly one - * backend (see `simplex_backend` / `SIMPLEX_BACKEND` at install time); this - * type makes the caller state which one they are targeting so field names - * can't lie about their meaning. + * Database configuration. `type` selects the libsimplex backend loaded by + * `ChatApi.init`; one backend per process. */ export type DbConfig = | { @@ -103,7 +101,7 @@ export class ChatApi { private constructor(protected ctrl_: bigint | undefined) {} /** - * Initializes the ChatApi. + * Initializes the ChatApi, loading libsimplex for `db.type` (downloaded on first use). * @param {DbConfig} db - Database configuration (sqlite or postgres). * @param {core.MigrationConfirmation} [confirm=core.MigrationConfirmation.YesUp] - Migration confirmation mode. * @param {number} [queueSize] - Size of internal queues, the core default is used when omitted. @@ -114,6 +112,7 @@ export class ChatApi { queueSize?: number ): Promise { const [path, key] = dbConfigToMigrateArgs(db) + await core.loadLibrary(db.type) const ctrl = await core.chatMigrateInit(path, key, confirm, queueSize) return new ChatApi(ctrl) } diff --git a/packages/simplex-chat-nodejs/src/core.ts b/packages/simplex-chat-nodejs/src/core.ts index 81f59942b0..c9ac3642f0 100644 --- a/packages/simplex-chat-nodejs/src/core.ts +++ b/packages/simplex-chat-nodejs/src/core.ts @@ -1,8 +1,27 @@ import {ChatEvent, ChatResponse, T} from "@simplex-chat/types" +import * as libs from "./libs" import * as simplex from "./simplex" export type Backend = "sqlite" | "postgres" +let loading: {backend: Backend, promise: Promise} | undefined + +/** + * Resolve (downloading on first use) and load libsimplex for the backend. + * One libsimplex per process: the Haskell runtime is initialized once, and another backend is rejected. + */ +export function loadLibrary(backend: Backend): Promise { + if (loading) { + if (loading.backend === backend) return loading.promise + return Promise.reject(new Error(`libsimplex already loaded with backend=${loading.backend}; cannot switch to ${backend} in the same process`)) + } + const promise = libs.resolveLibsDir(backend).then(dir => simplex.load(libs.libPath(dir))) + loading = {backend, promise} + // a failed download or load can be retried + promise.catch(() => { loading = undefined }) + return promise +} + /** * Initialize chat controller * @param {number} [queueSize] - Size of internal queues, the core default is used when omitted. diff --git a/packages/simplex-chat-nodejs/src/download-libs.js b/packages/simplex-chat-nodejs/src/download-libs.js deleted file mode 100644 index cb92699e69..0000000000 --- a/packages/simplex-chat-nodejs/src/download-libs.js +++ /dev/null @@ -1,266 +0,0 @@ -const https = require('https'); -const fs = require('fs'); -const path = require('path'); -const extract = require('extract-zip'); - -const GITHUB_REPO = 'simplex-chat/simplex-chat-libs'; -const RELEASE_TAG = 'v7.1.0-beta.4'; -const BACKEND = (process.env.SIMPLEX_BACKEND || process.env.npm_config_simplex_backend || 'sqlite').toLowerCase(); -// A locally built libsimplex, copied into libs/ rather than loaded in place: -// the addon's RUNPATH is $ORIGIN/../../libs. -const LIBS_DIR_OVERRIDE = process.env.SIMPLEX_LIBS_DIR || process.env.npm_config_simplex_libs_dir; -const LIB_NAMES = ['libsimplex.so', 'libsimplex.dylib', 'libsimplex.dll']; - -if (BACKEND !== 'sqlite' && BACKEND !== 'postgres') { - console.error(`✗ Invalid SIMPLEX_BACKEND: "${BACKEND}". Must be "sqlite" or "postgres".`); - process.exit(1); -} - -if (BACKEND === 'postgres' && (process.platform !== 'linux' || process.arch !== 'x64')) { - console.error(`✗ SIMPLEX_BACKEND=postgres is only supported on Linux x86_64.`); - process.exit(1); -} - -const ROOT_DIR = process.cwd(); // Root of the package being installed -const LIBS_DIR = path.join(ROOT_DIR, 'libs') -const INSTALLED_FILE = path.join(LIBS_DIR, 'installed.txt'); - -// Detect platform and architecture -function getPlatformInfo() { - const platform = process.platform; - const arch = process.arch; - - let platformName; - let archName; - - if (platform === 'linux') { - platformName = 'linux'; - } else if (platform === 'darwin') { - platformName = 'macos'; - } else if (platform === 'win32') { - platformName = 'windows'; - } else { - throw new Error(`Unsupported platform: ${platform}`); - } - - if (arch === 'x64') { - archName = 'x86_64'; - } else if (arch === 'arm64') { - archName = 'aarch64'; - } else { - throw new Error(`Unsupported architecture: ${arch}`); - } - - return { platformName, archName }; -} - -// Cleanup on libs version mismatch -function cleanLibsDirectory() { - if (fs.existsSync(LIBS_DIR)) { - console.log('Cleaning old libraries...'); - fs.rmSync(LIBS_DIR, { recursive: true, force: true }); - fs.mkdirSync(LIBS_DIR, { recursive: true }); - console.log('✓ Old libraries removed'); - } -} - -// Check if libraries are already installed with the correct version -function isAlreadyInstalled() { - if (!fs.existsSync(INSTALLED_FILE)) { - return false; - } - - try { - const installedVersion = fs.readFileSync(INSTALLED_FILE, 'utf-8').trim(); - const expectedVersion = `${RELEASE_TAG}:${BACKEND}`; - if (installedVersion === expectedVersion) { - console.log(`✓ Libraries version ${RELEASE_TAG}:${BACKEND} already installed`); - return true; - } else { - console.log(`Version mismatch: installed ${installedVersion}, need ${expectedVersion}`); - cleanLibsDirectory(); - return false; - } - } catch (err) { - console.warn(`Could not read installed.txt: ${err.message}`); - return false; - } -} - -// No version check: the files behind SIMPLEX_LIBS_DIR change on every rebuild. -function installFromOverride() { - if (!fs.existsSync(LIBS_DIR_OVERRIDE)) { - throw new Error(`SIMPLEX_LIBS_DIR does not exist: ${LIBS_DIR_OVERRIDE}`); - } - const lib = LIB_NAMES.find((name) => fs.existsSync(path.join(LIBS_DIR_OVERRIDE, name))); - if (!lib) { - throw new Error(`No ${LIB_NAMES.join(' / ')} in SIMPLEX_LIBS_DIR: ${LIBS_DIR_OVERRIDE}`); - } - console.log(`Using libraries from SIMPLEX_LIBS_DIR: ${LIBS_DIR_OVERRIDE}`); - cleanLibsDirectory(); - copyDirSync(LIBS_DIR_OVERRIDE, LIBS_DIR); - // Not a release tag: a later install without the variable sees a mismatch - // and replaces these files with the released ones. - fs.writeFileSync(INSTALLED_FILE, `${LIBS_DIR_OVERRIDE}:${BACKEND}`, 'utf-8'); - console.log(`✓ Installed ${lib} and its runtime libraries from ${LIBS_DIR_OVERRIDE}`); -} - -async function install() { - try { - if (LIBS_DIR_OVERRIDE) { - installFromOverride(); - return; - } - - // Check if already installed - if (isAlreadyInstalled()) { - return; - } - - const { platformName, archName } = getPlatformInfo(); - const repoName = GITHUB_REPO.split('/')[1]; - const backendSuffix = BACKEND === 'postgres' ? '-postgres' : ''; - const zipFilename = `${repoName}-${platformName}-${archName}${backendSuffix}.zip`; - const ZIP_URL = `https://github.com/${GITHUB_REPO}/releases/download/${RELEASE_TAG}/${zipFilename}`; - const ZIP_PATH = path.join(ROOT_DIR, zipFilename); - const TEMP_EXTRACT_DIR = path.join(ROOT_DIR, '.temp-extract'); - - console.log(`Detected: ${platformName} ${archName}`); - console.log(`Backend: ${BACKEND}`); - console.log(`Downloading: ${zipFilename}`); - - // Create libs directory - if (!fs.existsSync(LIBS_DIR)) { - fs.mkdirSync(LIBS_DIR, { recursive: true }); - } - - // Download zip with error handling - await downloadFile(ZIP_URL, ZIP_PATH); - - // Extract to temporary directory - console.log('Extracting to temporary directory...'); - if (!fs.existsSync(TEMP_EXTRACT_DIR)) { - fs.mkdirSync(TEMP_EXTRACT_DIR, { recursive: true }); - } - await extract(ZIP_PATH, { dir: TEMP_EXTRACT_DIR }); - - // Move libs folder contents to final location - console.log('Moving libraries to libs/...'); - const libsSourcePath = path.join(TEMP_EXTRACT_DIR, 'libs'); - - if (fs.existsSync(libsSourcePath)) { - // Copy all files from libs folder to LIBS_DIR - const files = fs.readdirSync(libsSourcePath); - files.forEach(file => { - const src = path.join(libsSourcePath, file); - const dest = path.join(LIBS_DIR, file); - - if (fs.statSync(src).isDirectory()) { - copyDirSync(src, dest); - } else { - fs.copyFileSync(src, dest); - } - }); - } else { - throw new Error('libs folder not found in zip archive'); - } - - // Write installed.txt with version - fs.writeFileSync(INSTALLED_FILE, `${RELEASE_TAG}:${BACKEND}`, 'utf-8'); - console.log(`✓ Wrote version ${RELEASE_TAG}:${BACKEND} to installed.txt`); - - // Cleanup - fs.rmSync(TEMP_EXTRACT_DIR, { recursive: true, force: true }); - fs.unlinkSync(ZIP_PATH); - console.log('✓ Installation complete'); - } catch (err) { - console.error('✗ Failed:', err.message); - process.exit(1); - } -} - -// Helper function to recursively copy directories -function copyDirSync(src, dest) { - if (!fs.existsSync(dest)) { - fs.mkdirSync(dest, { recursive: true }); - } - const files = fs.readdirSync(src); - files.forEach(file => { - const srcFile = path.join(src, file); - const destFile = path.join(dest, file); - if (fs.statSync(srcFile).isDirectory()) { - copyDirSync(srcFile, destFile); - } else { - fs.copyFileSync(srcFile, destFile); - } - }); -} - -function downloadFile(url, dest) { - return new Promise((resolve, reject) => { - const file = fs.createWriteStream(dest); - - https.get(url, { headers: { 'User-Agent': 'Node.js' } }, (response) => { - // Handle redirects - if (response.statusCode === 302 || response.statusCode === 301) { - file.destroy(); - fs.unlink(dest, () => {}); - return downloadFile(response.headers.location, dest) - .then(resolve) - .catch(reject); - } - - // Handle 404 - if (response.statusCode === 404) { - file.destroy(); - fs.unlink(dest, () => {}); - reject(new Error( - `Release artifact not found (404). Check:\n` + - ` - Repository exists: ${url.split('/releases')[0]}\n` + - ` - Release tag exists: ${RELEASE_TAG}\n` + - ` - Artifact filename is correct` - )); - return; - } - - // Handle 403 - if (response.statusCode === 403) { - file.destroy(); - fs.unlink(dest, () => {}); - reject(new Error( - `Access denied (403). The repository may be private.\n` + - `Set GITHUB_TOKEN environment variable for private repos.` - )); - return; - } - - // Handle other HTTP errors - if (response.statusCode < 200 || response.statusCode >= 300) { - file.destroy(); - fs.unlink(dest, () => {}); - reject(new Error( - `HTTP ${response.statusCode}: Failed to download from ${url}` - )); - return; - } - - response.pipe(file); - - file.on('finish', () => { - file.close(); - resolve(); - }); - - file.on('error', (err) => { - fs.unlink(dest, () => {}); - reject(new Error(`File write error: ${err.message}`)); - }); - }).on('error', (err) => { - file.destroy(); - fs.unlink(dest, () => {}); - reject(new Error(`Download error: ${err.message}`)); - }); - }); -} - -install(); diff --git a/packages/simplex-chat-nodejs/src/simplex.d.ts b/packages/simplex-chat-nodejs/src/simplex.d.ts index 1e0ca825a6..d7575a8677 100644 --- a/packages/simplex-chat-nodejs/src/simplex.d.ts +++ b/packages/simplex-chat-nodejs/src/simplex.d.ts @@ -1,5 +1,6 @@ // These functions are defined in CPP add-on ../cpp/simplex.cc +export function load(libPath: string): void export function chat_migrate_init(dbPath: string, dbKey: string, confirm: string): Promise<[bigint, string]> export function chat_migrate_init_queue(dbPath: string, dbKey: string, confirm: string, queueSize: number): Promise<[bigint, string]> export function chat_close_store(ctrl: bigint): Promise diff --git a/packages/simplex-chat-nodejs/tests/api.unit.test.ts b/packages/simplex-chat-nodejs/tests/api.unit.test.ts index c5a4367b11..b84de92b09 100644 --- a/packages/simplex-chat-nodejs/tests/api.unit.test.ts +++ b/packages/simplex-chat-nodejs/tests/api.unit.test.ts @@ -5,6 +5,7 @@ import * as core from "../src/core" const user = {userId: 1} as T.User async function chatWithResponse(response: object): Promise { + jest.spyOn(core, "loadLibrary").mockResolvedValue() jest.spyOn(core, "chatMigrateInit").mockResolvedValue(BigInt(1)) jest.spyOn(core, "chatSendCmd").mockResolvedValue(response as ChatResponse) return api.ChatApi.init({type: "sqlite", filePrefix: "unused"}) @@ -38,6 +39,7 @@ describe("documented success responses", () => { describe("startChat lifecycle", () => { function chatWithResponses(...responses: object[]): Promise { + jest.spyOn(core, "loadLibrary").mockResolvedValue() jest.spyOn(core, "chatMigrateInit").mockResolvedValue(BigInt(1)) jest.spyOn(core, "chatRecvMsgWait").mockImplementation(() => new Promise(resolve => setTimeout(() => resolve(undefined), 10))) const send = jest.spyOn(core, "chatSendCmd") @@ -100,3 +102,24 @@ describe("startChat lifecycle", () => { expect(core.chatRecvMsgWait).toHaveBeenCalledWith(BigInt(1), 500_000) }) }) + +describe("ChatApi.init", () => { + it("loads the library for the configured backend before opening the database", async () => { + let loaded!: () => void + const load = jest.spyOn(core, "loadLibrary").mockReturnValue(new Promise(resolve => { loaded = resolve })) + const migrate = jest.spyOn(core, "chatMigrateInit").mockResolvedValue(BigInt(1)) + const init = api.ChatApi.init({type: "postgres", connectionString: "postgres://unused"}) + await new Promise(setImmediate) + expect(load).toHaveBeenCalledWith("postgres") + expect(migrate).not.toHaveBeenCalled() + loaded() + await init + expect(migrate).toHaveBeenCalled() + }) + + it("rejects an invalid config before loading the library", async () => { + const load = jest.spyOn(core, "loadLibrary").mockResolvedValue() + await expect(api.ChatApi.init({type: "mysql"} as unknown as api.DbConfig)).rejects.toThrow('Invalid DbConfig: {"type":"mysql"}') + expect(load).not.toHaveBeenCalled() + }) +}) diff --git a/packages/simplex-chat-nodejs/tests/core.test.ts b/packages/simplex-chat-nodejs/tests/core.test.ts index 8eb106a5b5..0a1bcc08df 100644 --- a/packages/simplex-chat-nodejs/tests/core.test.ts +++ b/packages/simplex-chat-nodejs/tests/core.test.ts @@ -2,8 +2,16 @@ import {execFile, spawnSync} from "child_process"; import * as fs from "fs"; import * as path from "path"; import {core} from "../src/index"; +import * as libs from "../src/libs"; +import * as simplex from "../src/simplex"; describe("Core tests", () => { + let libPath: string; + // the first run downloads libsimplex + beforeAll(async () => { + libPath = libs.libPath(await libs.resolveLibsDir("sqlite")); + await core.loadLibrary("sqlite"); + }, 300000); const tmpDir = "./tests/tmp"; const dbPath = path.join(tmpDir, "simplex_v1"); @@ -191,10 +199,42 @@ describe("Core tests", () => { expect(await receives).toEqual([{event: undefined}, {error: "chat receiver stopped"}]); }, 10000); + it("should accept loading libsimplex again from the same path", () => { + expect(() => simplex.load(libPath)).not.toThrow(); + }); + + it("should refuse to load libsimplex from another path", () => { + expect(() => simplex.load(path.resolve("other", "libsimplex.so"))).toThrow(`libsimplex already loaded from ${libPath}`); + }); + + it("should load libsimplex after a failed load and in several workers", () => { + const addon = path.resolve(__dirname, "..", "build", "Release", "simplex.node"); + const childDbPath = path.resolve(tmpDir, "simplex_workers"); + const script = ` + const {Worker} = require("worker_threads"); + const simplex = require(${JSON.stringify(addon)}); + try { simplex.load(${JSON.stringify(addon)}) } catch (e) { console.log(e.message.endsWith("does not export hs_init_with_rtsopts")) } + const load = ${JSON.stringify(`require(${JSON.stringify(addon)}).load(${JSON.stringify(libPath)})`)}; + const workers = Array.from({length: 4}, () => new Worker(load, {eval: true})); + Promise.all(workers.map((w) => new Promise((resolve, reject) => w.on("exit", resolve).on("error", reject)))) + .then(async (codes) => { + simplex.load(${JSON.stringify(libPath)}); + const [ctrl] = await simplex.chat_migrate_init(${JSON.stringify(childDbPath)}, "key", "yesUp"); + await simplex.chat_send_cmd(ctrl, "/_stop"); + console.log(JSON.stringify(codes), await simplex.chat_close_store(ctrl) === ""); + }); + `; + const child = spawnSync(process.execPath, ["-e", script], {timeout: 30000, encoding: "utf8"}); + if (child.status !== 0 || child.signal !== null) console.log("child stderr:", child.stderr); + expect({status: child.status, signal: child.signal, stdout: child.stdout.trim()}) + .toEqual({status: 0, signal: null, stdout: "true\n[0,0,0,0] true"}); + }, 35000); + it("should let the process exit while a receiver is idle", () => { const childDbPath = path.resolve(tmpDir, "simplex_child"); const script = ` const simplex = require("./build/Release/simplex.node"); + simplex.load(${JSON.stringify(libPath)}); simplex.chat_migrate_init(${JSON.stringify(childDbPath)}, "key", "yesUp") .then(([ctrl]) => simplex.chat_recv_msg_wait(ctrl, 1)) .then((res) => console.log("received " + JSON.stringify(res))); @@ -209,6 +249,7 @@ describe("Core tests", () => { const script = ` const fs = require("fs"), path = require("path"); const simplex = require("./build/Release/simplex.node"); + simplex.load(${JSON.stringify(libPath)}); (async () => { for (let i = 0; i < 40; i++) { const dir = fs.mkdtempSync(path.join(${JSON.stringify(path.resolve(tmpDir))}, "close-")); diff --git a/packages/simplex-chat-nodejs/tests/loader.test.ts b/packages/simplex-chat-nodejs/tests/loader.test.ts new file mode 100644 index 0000000000..167d65258f --- /dev/null +++ b/packages/simplex-chat-nodejs/tests/loader.test.ts @@ -0,0 +1,97 @@ +import {spawnSync} from "child_process" +import * as path from "path" + +jest.mock("../src/simplex", () => ({load: jest.fn()})) +jest.mock("../src/libs", () => ({libPath: jest.requireActual("../src/libs").libPath, resolveLibsDir: jest.fn()})) + +const PACKAGE_DIR = path.join(__dirname, "..") +const ADDON = path.join(PACKAGE_DIR, "build", "Release", "simplex.node") +const CHILD_TIMEOUT_MS = 10000 +const LIBS_DIR = path.resolve("libs") +const SWITCH_ERROR = "libsimplex already loaded with backend=sqlite; cannot switch to postgres in the same process" +const {libPath} = jest.requireActual("../src/libs") + +function runAddon(script: string): string { + const r = spawnSync(process.execPath, ["-e", `const simplex = require(${JSON.stringify(ADDON)});\n${script}`], {encoding: "utf8", timeout: CHILD_TIMEOUT_MS}) + return `${r.stdout}${r.stderr}status=${r.status} signal=${r.signal}` +} + +function freshCore(): {core: typeof import("../src/core"), resolveLibsDir: jest.Mock, load: jest.Mock} { + let modules!: ReturnType + jest.isolateModules(() => { + modules = { + core: require("../src/core"), + resolveLibsDir: require("../src/libs").resolveLibsDir, + load: require("../src/simplex").load, + } + }) + return modules +} + +describe("native load", () => { + it("requires load before every FFI call", () => { + const out = runAddon(` + for (const name of Object.keys(simplex).filter(n => n !== "load").sort()) { + try { simplex[name](); console.log(name + ": no error") } catch (e) { console.log(name + ": " + e.message) } + }`) + const bindings = ["chat_close_store", "chat_decrypt_file", "chat_encrypt_file", "chat_migrate_init", "chat_migrate_init_queue", + "chat_read_file", "chat_recv_msg_wait", "chat_send_cmd", "chat_write_file"] + const expected = bindings.map(name => `${name}: libsimplex is not loaded, call core.loadLibrary(backend) first\n`).join("") + expect(out).toBe(`${expected}status=0 signal=null`) + }) + + it("reports the path of a library that cannot be opened", () => { + const out = runAddon(`try { simplex.load("/nonexistent/libsimplex.so") } catch (e) { console.log(e.message) }`) + expect(out).toContain("cannot load /nonexistent/libsimplex.so") + }) + + it("rejects a non-string library path", () => { + const out = runAddon(`try { simplex.load(1) } catch (e) { console.log(e.name + ": " + e.message) }`) + expect(out).toContain("TypeError: Expected string (libPath)") + }) + + it("rejects a library without the chat exports", () => { + const out = runAddon(`try { simplex.load(${JSON.stringify(ADDON)}) } catch (e) { console.log(e.message) }`) + expect(out).toContain(`${ADDON} does not export hs_init_with_rtsopts`) + }) +}) + +describe("loadLibrary", () => { + it("shares one load between concurrent calls", async () => { + const {core, resolveLibsDir, load} = freshCore() + resolveLibsDir.mockResolvedValue(LIBS_DIR) + const first = core.loadLibrary("sqlite") + const second = core.loadLibrary("sqlite") + expect(second).toBe(first) + await Promise.all([first, second]) + expect(resolveLibsDir).toHaveBeenCalledTimes(1) + expect(load.mock.calls).toEqual([[libPath(LIBS_DIR)]]) + }) + + it("refuses to switch backend while the first load is in progress", async () => { + const {core, resolveLibsDir} = freshCore() + resolveLibsDir.mockResolvedValue(LIBS_DIR) + const first = core.loadLibrary("sqlite") + await expect(core.loadLibrary("postgres")).rejects.toThrow(SWITCH_ERROR) + await first + }) + + it("keeps a completed load for the process", async () => { + const {core, resolveLibsDir, load} = freshCore() + resolveLibsDir.mockResolvedValue(LIBS_DIR) + await core.loadLibrary("sqlite") + await core.loadLibrary("sqlite") + await expect(core.loadLibrary("postgres")).rejects.toThrow(SWITCH_ERROR) + expect(resolveLibsDir).toHaveBeenCalledTimes(1) + expect(load).toHaveBeenCalledTimes(1) + }) + + it("retries after a failed load", async () => { + const {core, resolveLibsDir, load} = freshCore() + resolveLibsDir.mockRejectedValueOnce(new Error("HTTP 503")).mockResolvedValue(LIBS_DIR) + await expect(core.loadLibrary("sqlite")).rejects.toThrow("HTTP 503") + await expect(core.loadLibrary("postgres")).resolves.toBeUndefined() + expect(resolveLibsDir.mock.calls).toEqual([["sqlite"], ["postgres"]]) + expect(load).toHaveBeenCalledTimes(1) + }) +})