simplex-chat 7.1.0-beta.2 → 7.1.0-beta.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/binding.gyp +2 -0
- package/cpp/simplex.cc +265 -29
- package/cpp/simplex.h +2 -0
- package/dist/api.d.ts +9 -6
- package/dist/api.js +37 -19
- package/dist/api.js.map +1 -1
- package/dist/bot.d.ts +6 -2
- package/dist/bot.js +17 -34
- package/dist/bot.js.map +1 -1
- package/dist/core.d.ts +11 -7
- package/dist/core.js +5 -2
- package/dist/core.js.map +1 -1
- package/dist/simplex.d.ts +3 -2
- package/dist/util.js +1 -1
- package/dist/util.js.map +1 -1
- package/docs/Namespace.bot.md +1 -0
- package/docs/api.Class.ChatApi.md +122 -78
- package/docs/api.TypeAlias.EventSubscriberFunc.md +5 -1
- package/docs/bot.Function.run.md +1 -1
- package/docs/bot.Function.subscribeChatItems.md +27 -0
- package/docs/bot.Interface.BotConfig.md +13 -9
- package/docs/bot.Interface.BotOptions.md +10 -10
- package/docs/bot.TypeAlias.BotDbOpts.md +4 -0
- package/docs/core.Class.ChatAPIError.md +4 -4
- package/docs/core.Class.ChatInitError.md +4 -4
- package/docs/core.DBMigrationError.Interface.ErrorMigration.md +4 -4
- package/docs/core.DBMigrationError.Interface.ErrorNotADatabase.md +3 -3
- package/docs/core.DBMigrationError.Interface.ErrorSQL.md +4 -4
- package/docs/core.DBMigrationError.Interface.InvalidConfirmation.md +2 -2
- package/docs/core.DBMigrationError.Interface.InvalidQueueSize.md +25 -0
- package/docs/core.DBMigrationError.TypeAlias.Tag.md +2 -2
- package/docs/core.Enumeration.MigrationConfirmation.md +5 -5
- package/docs/core.Function.chatCloseStore.md +1 -1
- package/docs/core.Function.chatDecryptFile.md +1 -1
- package/docs/core.Function.chatEncryptFile.md +1 -1
- package/docs/core.Function.chatMigrateInit.md +8 -2
- package/docs/core.Function.chatReadFile.md +3 -3
- package/docs/core.Function.chatRecvMsgWait.md +1 -1
- package/docs/core.Function.chatSendCmd.md +1 -1
- package/docs/core.Function.chatWriteFile.md +2 -2
- package/docs/core.Interface.APIResult.md +3 -3
- package/docs/core.Interface.CryptoArgs.md +3 -3
- package/docs/core.Interface.UpMigration.md +3 -3
- package/docs/core.MTRError.Interface.MTREDifferent.md +3 -3
- package/docs/core.MTRError.Interface.MTRENoDown.md +10 -10
- package/docs/core.MTRError.TypeAlias.Tag.md +1 -1
- package/docs/core.MigrationError.Interface.MEDowngrade.md +3 -3
- package/docs/core.MigrationError.Interface.MEUpgrade.md +4 -4
- package/docs/core.MigrationError.Interface.MigrationError.md +3 -3
- package/docs/core.MigrationError.TypeAlias.Tag.md +1 -1
- package/docs/core.Namespace.DBMigrationError.md +1 -0
- package/docs/core.TypeAlias.DBMigrationError.md +2 -2
- package/docs/core.TypeAlias.MTRError.md +1 -1
- package/docs/core.TypeAlias.MigrationError.md +1 -1
- package/examples/squaring-bot-readme.js +2 -2
- package/examples/squaring-bot.ts +4 -4
- package/package.json +2 -2
- package/src/api.ts +54 -25
- package/src/bot.ts +43 -37
- package/src/core.ts +15 -7
- package/src/download-libs.js +28 -1
- package/src/simplex.d.ts +3 -2
- package/src/util.ts +3 -3
package/README.md
CHANGED
|
@@ -24,13 +24,13 @@ Simple bot that replies with squares of numbers you send to it:
|
|
|
24
24
|
const {bot} = await import("simplex-chat")
|
|
25
25
|
// if you are running from this GitHub repo:
|
|
26
26
|
// const {bot} = await import("../dist/index.js")
|
|
27
|
-
|
|
27
|
+
await bot.run({
|
|
28
28
|
profile: {displayName: "Squaring bot example", fullName: ""},
|
|
29
29
|
dbOpts: {type: "sqlite", filePrefix: "./squaring_bot"},
|
|
30
30
|
options: {
|
|
31
31
|
addressSettings: {welcomeMessage: "Send a number, I will square it.",
|
|
32
32
|
},
|
|
33
|
-
onMessage: async (ci, content) => {
|
|
33
|
+
onMessage: async (ci, content, chat) => {
|
|
34
34
|
const n = +content.text
|
|
35
35
|
const reply = typeof n === "number" && !isNaN(n)
|
|
36
36
|
? `${n} * ${n} = ${n * n}`
|
|
@@ -62,6 +62,8 @@ There is an example with more options in [./examples/squaring-bot.ts](./examples
|
|
|
62
62
|
|
|
63
63
|
You can run it with: `npx ts-node ./examples/squaring-bot.ts`
|
|
64
64
|
|
|
65
|
+
A larger example, with a business address and commands as keys: [SimpleX Calculator](../../apps/simplex-calculator-bot/).
|
|
66
|
+
|
|
65
67
|
## PostgreSQL backend
|
|
66
68
|
|
|
67
69
|
By default, the package uses SQLite. To use PostgreSQL instead:
|
package/binding.gyp
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
],
|
|
12
12
|
"cflags!": [ "-fno-exceptions" ],
|
|
13
13
|
"cflags_cc!": [ "-fno-exceptions" ],
|
|
14
|
+
"xcode_settings": { "GCC_ENABLE_CPP_EXCEPTIONS": "YES" },
|
|
15
|
+
"msvs_settings": { "VCCLCompilerTool": { "ExceptionHandling": 1 } },
|
|
14
16
|
"defines": [ "NAPI_DISABLE_CPP_EXCEPTIONS" ],
|
|
15
17
|
"conditions": [
|
|
16
18
|
["OS=='mac'", {
|
package/cpp/simplex.cc
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
#include <string>
|
|
4
4
|
#include <functional>
|
|
5
5
|
#include <cstdlib>
|
|
6
|
+
#include <climits>
|
|
7
|
+
#include <memory>
|
|
8
|
+
#include <thread>
|
|
9
|
+
#include <system_error>
|
|
10
|
+
#include <mutex>
|
|
11
|
+
#include <condition_variable>
|
|
12
|
+
#include <deque>
|
|
13
|
+
#include <unordered_map>
|
|
6
14
|
#include "simplex.h"
|
|
7
15
|
|
|
8
16
|
namespace simplex {
|
|
@@ -81,6 +89,11 @@ class ResultAsyncWorker : public AsyncWorker {
|
|
|
81
89
|
return ctrl_;
|
|
82
90
|
}
|
|
83
91
|
|
|
92
|
+
// the worker thread reads this object's memory until the worker completes
|
|
93
|
+
void KeepAlive(Object obj) {
|
|
94
|
+
keep_alive_ = Persistent(obj);
|
|
95
|
+
}
|
|
96
|
+
|
|
84
97
|
protected:
|
|
85
98
|
std::string result_;
|
|
86
99
|
uintptr_t ctrl_ = 0;
|
|
@@ -88,6 +101,7 @@ class ResultAsyncWorker : public AsyncWorker {
|
|
|
88
101
|
private:
|
|
89
102
|
ExecuteFn execute_fn_;
|
|
90
103
|
ResultProcessor result_processor_;
|
|
104
|
+
ObjectReference keep_alive_;
|
|
91
105
|
};
|
|
92
106
|
|
|
93
107
|
class BinaryAsyncWorker : public AsyncWorker {
|
|
@@ -97,21 +111,25 @@ class BinaryAsyncWorker : public AsyncWorker {
|
|
|
97
111
|
BinaryAsyncWorker(Function& callback, ExecuteFn execute_fn)
|
|
98
112
|
: AsyncWorker(callback), execute_fn_(std::move(execute_fn)) {}
|
|
99
113
|
|
|
114
|
+
~BinaryAsyncWorker() {
|
|
115
|
+
free(original_buf);
|
|
116
|
+
}
|
|
117
|
+
|
|
100
118
|
void Execute() override {
|
|
101
119
|
execute_fn_(this);
|
|
102
120
|
}
|
|
103
121
|
|
|
104
122
|
void OnOK() override {
|
|
105
123
|
HandleScope scope(Env());
|
|
106
|
-
|
|
107
|
-
|
|
124
|
+
char* buf = original_buf;
|
|
125
|
+
original_buf = nullptr;
|
|
126
|
+
if (binary_len == 0) {
|
|
127
|
+
free(buf);
|
|
128
|
+
Callback().Call({Env().Null(), Buffer<char>::New(Env(), 0)});
|
|
108
129
|
return;
|
|
109
130
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
free(orig);
|
|
113
|
-
};
|
|
114
|
-
Napi::Buffer<char> buffer = Napi::Buffer<char>::New(Env(), data_ptr, binary_len, finalizer, original_buf);
|
|
131
|
+
// Copies when the runtime forbids external buffers (Electron); the finalizer then runs immediately.
|
|
132
|
+
Buffer<char> buffer = Buffer<char>::NewOrCopy(Env(), buf + 5, binary_len, [](Napi::Env, char*, char* orig) { free(orig); }, buf);
|
|
115
133
|
Callback().Call({Env().Null(), buffer});
|
|
116
134
|
}
|
|
117
135
|
|
|
@@ -176,6 +194,159 @@ Napi::Promise CreatePromiseAndCallback(Env env, Function& cb_out) {
|
|
|
176
194
|
return deferred.Promise();
|
|
177
195
|
}
|
|
178
196
|
|
|
197
|
+
const char* const RECEIVER_STOPPED = "chat receiver stopped";
|
|
198
|
+
|
|
199
|
+
struct RecvRequest {
|
|
200
|
+
int wait = 0;
|
|
201
|
+
std::shared_ptr<Promise::Deferred> deferred;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Holds the event loop open only while receives are pending; used only on the JS main thread.
|
|
205
|
+
class PendingReceives {
|
|
206
|
+
public:
|
|
207
|
+
explicit PendingReceives(ThreadSafeFunction tsfn) : tsfn_(tsfn) {}
|
|
208
|
+
|
|
209
|
+
const ThreadSafeFunction& Tsfn() const {
|
|
210
|
+
return tsfn_;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
void Add(Napi::Env env) {
|
|
214
|
+
if (count_++ == 0) tsfn_.Ref(env);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
void Remove(Napi::Env env) {
|
|
218
|
+
if (--count_ == 0) tsfn_.Unref(env);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private:
|
|
222
|
+
ThreadSafeFunction tsfn_;
|
|
223
|
+
size_t count_ = 0;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// A blocking receive would hold a libuv pool thread for up to `wait`, stalling fs, dns and crypto.
|
|
227
|
+
class Receiver {
|
|
228
|
+
public:
|
|
229
|
+
// Returns nullptr with a pending JS exception if the TSFN cannot be created, throws std::system_error if the thread cannot start.
|
|
230
|
+
static std::shared_ptr<Receiver> Start(Napi::Env env, chat_ctrl ctrl) {
|
|
231
|
+
ThreadSafeFunction tsfn = ThreadSafeFunction::New(env, Function::New(env, [](const CallbackInfo&) {}), "chat_recv_msg_wait", 0, 1);
|
|
232
|
+
if (env.IsExceptionPending()) {
|
|
233
|
+
return nullptr;
|
|
234
|
+
}
|
|
235
|
+
auto receiver = std::make_shared<Receiver>(ctrl, tsfn);
|
|
236
|
+
try {
|
|
237
|
+
receiver->thread_ = std::thread(&Receiver::Run, receiver.get());
|
|
238
|
+
} catch (const std::system_error&) {
|
|
239
|
+
tsfn.Release();
|
|
240
|
+
throw;
|
|
241
|
+
}
|
|
242
|
+
return receiver;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
Receiver(chat_ctrl ctrl, ThreadSafeFunction tsfn) : ctrl_(ctrl), pending_(std::make_shared<PendingReceives>(tsfn)) {}
|
|
246
|
+
|
|
247
|
+
Receiver(const Receiver&) = delete;
|
|
248
|
+
Receiver& operator=(const Receiver&) = delete;
|
|
249
|
+
|
|
250
|
+
~Receiver() {
|
|
251
|
+
Stop();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
void Enqueue(Napi::Env env, RecvRequest request) {
|
|
255
|
+
pending_->Add(env);
|
|
256
|
+
{
|
|
257
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
258
|
+
queue_.push_back(std::move(request));
|
|
259
|
+
}
|
|
260
|
+
cv_.notify_one();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
void RequestStop() {
|
|
264
|
+
{
|
|
265
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
266
|
+
stop_ = true;
|
|
267
|
+
}
|
|
268
|
+
cv_.notify_one();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Waits for the receive in progress, so it must not run on the JS main thread outside env teardown.
|
|
272
|
+
void Stop() {
|
|
273
|
+
RequestStop();
|
|
274
|
+
if (thread_.joinable()) {
|
|
275
|
+
thread_.join();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private:
|
|
280
|
+
void Run() {
|
|
281
|
+
for (;;) {
|
|
282
|
+
RecvRequest request;
|
|
283
|
+
{
|
|
284
|
+
std::unique_lock<std::mutex> lock(mutex_);
|
|
285
|
+
cv_.wait(lock, [this] { return stop_ || !queue_.empty(); });
|
|
286
|
+
if (stop_) break;
|
|
287
|
+
request = std::move(queue_.front());
|
|
288
|
+
queue_.pop_front();
|
|
289
|
+
}
|
|
290
|
+
char* c_res = chat_recv_msg_wait(ctrl_, request.wait);
|
|
291
|
+
napi_status status = Settle(request.deferred, [c_res](Napi::Env env, Promise::Deferred& deferred) {
|
|
292
|
+
if (c_res == nullptr) {
|
|
293
|
+
deferred.Reject(Error::New(env, "chat_recv_msg_wait failed").Value());
|
|
294
|
+
} else {
|
|
295
|
+
deferred.Resolve(String::New(env, c_res));
|
|
296
|
+
free(c_res);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
if (status != napi_ok) {
|
|
300
|
+
free(c_res);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
std::deque<RecvRequest> unserved;
|
|
304
|
+
{
|
|
305
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
306
|
+
unserved.swap(queue_);
|
|
307
|
+
}
|
|
308
|
+
for (RecvRequest& request : unserved) {
|
|
309
|
+
Settle(request.deferred, [](Napi::Env env, Promise::Deferred& deferred) {
|
|
310
|
+
deferred.Reject(Error::New(env, RECEIVER_STOPPED).Value());
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
pending_->Tsfn().Release();
|
|
314
|
+
// Each OS thread that enters Haskell keeps an RTS task record until it calls hs_thread_done.
|
|
315
|
+
hs_thread_done();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
template <typename SettleFn>
|
|
319
|
+
napi_status Settle(std::shared_ptr<Promise::Deferred> deferred, SettleFn settle) {
|
|
320
|
+
// The callback may run after this Receiver is destroyed, so it owns the pending count.
|
|
321
|
+
std::shared_ptr<PendingReceives> pending = pending_;
|
|
322
|
+
return pending->Tsfn().BlockingCall([pending, deferred, settle](Napi::Env env, Function) {
|
|
323
|
+
settle(env, *deferred);
|
|
324
|
+
pending->Remove(env);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const chat_ctrl ctrl_;
|
|
329
|
+
const std::shared_ptr<PendingReceives> pending_;
|
|
330
|
+
std::mutex mutex_;
|
|
331
|
+
std::condition_variable cv_;
|
|
332
|
+
std::deque<RecvRequest> queue_;
|
|
333
|
+
bool stop_ = false;
|
|
334
|
+
std::thread thread_;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// Keyed by chat_ctrl, accessed only on the JS main thread.
|
|
338
|
+
using Receivers = std::unordered_map<uintptr_t, std::shared_ptr<Receiver>>;
|
|
339
|
+
|
|
340
|
+
std::shared_ptr<Receiver> TakeReceiver(Receivers& receivers, chat_ctrl ctrl) {
|
|
341
|
+
auto it = receivers.find(reinterpret_cast<uintptr_t>(ctrl));
|
|
342
|
+
if (it == receivers.end()) {
|
|
343
|
+
return nullptr;
|
|
344
|
+
}
|
|
345
|
+
std::shared_ptr<Receiver> receiver = std::move(it->second);
|
|
346
|
+
receivers.erase(it);
|
|
347
|
+
return receiver;
|
|
348
|
+
}
|
|
349
|
+
|
|
179
350
|
// Common result processors
|
|
180
351
|
ResultAsyncWorker::ResultProcessor MigrateResultProcessor() {
|
|
181
352
|
return [](ResultAsyncWorker* worker, Napi::Env env) {
|
|
@@ -215,6 +386,39 @@ Value ChatMigrateInit(const CallbackInfo& args) {
|
|
|
215
386
|
return promise;
|
|
216
387
|
}
|
|
217
388
|
|
|
389
|
+
Value ChatMigrateInitQueue(const CallbackInfo& args) {
|
|
390
|
+
Env env = args.Env();
|
|
391
|
+
if (args.Length() < 4 || !args[0].IsString() || !args[1].IsString() || !args[2].IsString() || !args[3].IsNumber()) {
|
|
392
|
+
TypeError::New(env, "Expected three string arguments and number").ThrowAsJavaScriptException();
|
|
393
|
+
return env.Undefined();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
std::string path = args[0].As<String>().Utf8Value();
|
|
397
|
+
std::string key = args[1].As<String>().Utf8Value();
|
|
398
|
+
std::string confirm = args[2].As<String>().Utf8Value();
|
|
399
|
+
Number queue_size_arg = args[3].As<Number>();
|
|
400
|
+
int queue_size = queue_size_arg.Int32Value();
|
|
401
|
+
if (static_cast<double>(queue_size) != queue_size_arg.DoubleValue()) {
|
|
402
|
+
RangeError::New(env, "Expected 32-bit integer queue size").ThrowAsJavaScriptException();
|
|
403
|
+
return env.Undefined();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
Function cb;
|
|
407
|
+
Promise promise = CreatePromiseAndCallback(env, cb);
|
|
408
|
+
|
|
409
|
+
auto execute_fn = [path, key, confirm, queue_size](ResultAsyncWorker* worker) {
|
|
410
|
+
chat_ctrl ctrl = nullptr;
|
|
411
|
+
char* c_res = chat_migrate_init_queue(path.c_str(), key.c_str(), confirm.c_str(), queue_size, &ctrl);
|
|
412
|
+
worker->SetCtrl(reinterpret_cast<uintptr_t>(ctrl));
|
|
413
|
+
HandleCResult(worker, c_res, "chat_migrate_init_queue");
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn), MigrateResultProcessor());
|
|
417
|
+
worker->Queue();
|
|
418
|
+
|
|
419
|
+
return promise;
|
|
420
|
+
}
|
|
421
|
+
|
|
218
422
|
Value ChatCloseStore(const CallbackInfo& args) {
|
|
219
423
|
Env env = args.Env();
|
|
220
424
|
if (args.Length() < 1 || !args[0].IsBigInt()) {
|
|
@@ -223,11 +427,15 @@ Value ChatCloseStore(const CallbackInfo& args) {
|
|
|
223
427
|
}
|
|
224
428
|
|
|
225
429
|
chat_ctrl ctrl = FromChatCtrlBigInt(args[0]);
|
|
430
|
+
std::shared_ptr<Receiver> receiver = TakeReceiver(*static_cast<Receivers*>(args.Data()), ctrl);
|
|
226
431
|
|
|
227
432
|
Function cb;
|
|
228
433
|
Promise promise = CreatePromiseAndCallback(env, cb);
|
|
229
434
|
|
|
230
|
-
auto execute_fn = [ctrl](ResultAsyncWorker* worker) {
|
|
435
|
+
auto execute_fn = [ctrl, receiver](ResultAsyncWorker* worker) {
|
|
436
|
+
if (receiver) {
|
|
437
|
+
receiver->Stop();
|
|
438
|
+
}
|
|
231
439
|
char* c_res = chat_close_store(ctrl);
|
|
232
440
|
HandleCResult(worker, c_res, "chat_close_store");
|
|
233
441
|
};
|
|
@@ -271,44 +479,63 @@ Value ChatRecvMsgWait(const CallbackInfo& args) {
|
|
|
271
479
|
|
|
272
480
|
chat_ctrl ctrl = FromChatCtrlBigInt(args[0]);
|
|
273
481
|
int wait = static_cast<int>(args[1].As<Number>().Int32Value());
|
|
482
|
+
Receivers& receivers = *static_cast<Receivers*>(args.Data());
|
|
483
|
+
|
|
484
|
+
auto deferred = std::make_shared<Promise::Deferred>(Promise::Deferred::New(env));
|
|
485
|
+
auto it = receivers.find(reinterpret_cast<uintptr_t>(ctrl));
|
|
486
|
+
if (it == receivers.end()) {
|
|
487
|
+
std::shared_ptr<Receiver> receiver;
|
|
488
|
+
try {
|
|
489
|
+
receiver = Receiver::Start(env, ctrl);
|
|
490
|
+
} catch (const std::system_error& e) {
|
|
491
|
+
deferred->Reject(Error::New(env, e.what()).Value());
|
|
492
|
+
return deferred->Promise();
|
|
493
|
+
}
|
|
494
|
+
if (!receiver) {
|
|
495
|
+
return env.Undefined();
|
|
496
|
+
}
|
|
497
|
+
it = receivers.emplace(reinterpret_cast<uintptr_t>(ctrl), std::move(receiver)).first;
|
|
498
|
+
}
|
|
499
|
+
it->second->Enqueue(env, {wait, deferred});
|
|
274
500
|
|
|
275
|
-
|
|
276
|
-
Promise promise = CreatePromiseAndCallback(env, cb);
|
|
277
|
-
|
|
278
|
-
auto execute_fn = [ctrl, wait](ResultAsyncWorker* worker) {
|
|
279
|
-
char* c_res = chat_recv_msg_wait(ctrl, wait);
|
|
280
|
-
HandleCResult(worker, c_res, "chat_recv_msg_wait");
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn));
|
|
284
|
-
worker->Queue();
|
|
285
|
-
|
|
286
|
-
return promise;
|
|
501
|
+
return deferred->Promise();
|
|
287
502
|
}
|
|
288
503
|
|
|
289
504
|
Value ChatWriteFile(const CallbackInfo& args) {
|
|
290
505
|
Env env = args.Env();
|
|
291
|
-
if (args.Length() < 3 || !args[0].IsBigInt() || !args[1].IsString() || !args[2].IsArrayBuffer()) {
|
|
292
|
-
TypeError::New(env, "Expected bigint (ctrl), string (path), ArrayBuffer").ThrowAsJavaScriptException();
|
|
506
|
+
if (args.Length() < 3 || !args[0].IsBigInt() || !args[1].IsString() || !(args[2].IsArrayBuffer() || args[2].IsTypedArray())) {
|
|
507
|
+
TypeError::New(env, "Expected bigint (ctrl), string (path), ArrayBuffer or Uint8Array").ThrowAsJavaScriptException();
|
|
293
508
|
return env.Undefined();
|
|
294
509
|
}
|
|
295
510
|
|
|
296
511
|
chat_ctrl ctrl = FromChatCtrlBigInt(args[0]);
|
|
297
512
|
std::string path = args[1].As<String>().Utf8Value();
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
513
|
+
char* data;
|
|
514
|
+
size_t len;
|
|
515
|
+
if (args[2].IsArrayBuffer()) {
|
|
516
|
+
ArrayBuffer ab = args[2].As<ArrayBuffer>();
|
|
517
|
+
data = static_cast<char*>(ab.Data());
|
|
518
|
+
len = ab.ByteLength();
|
|
519
|
+
} else {
|
|
520
|
+
TypedArray view = args[2].As<TypedArray>();
|
|
521
|
+
data = static_cast<char*>(view.ArrayBuffer().Data()) + view.ByteOffset();
|
|
522
|
+
len = view.ByteLength();
|
|
523
|
+
}
|
|
524
|
+
if (len > static_cast<size_t>(INT_MAX)) {
|
|
525
|
+
RangeError::New(env, "Buffer is too large").ThrowAsJavaScriptException();
|
|
526
|
+
return env.Undefined();
|
|
527
|
+
}
|
|
301
528
|
|
|
302
529
|
Function cb;
|
|
303
530
|
Promise promise = CreatePromiseAndCallback(env, cb);
|
|
304
531
|
|
|
305
|
-
auto execute_fn = [ctrl, path,
|
|
306
|
-
(void)ab; // to keep ArrayBuffer alive
|
|
532
|
+
auto execute_fn = [ctrl, path, data, len](ResultAsyncWorker* worker) {
|
|
307
533
|
char* c_res = chat_write_file(ctrl, path.c_str(), data, static_cast<int>(len));
|
|
308
534
|
HandleCResult(worker, c_res, "chat_write_file");
|
|
309
535
|
};
|
|
310
536
|
|
|
311
537
|
ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn));
|
|
538
|
+
worker->KeepAlive(args[2].As<Object>());
|
|
312
539
|
worker->Queue();
|
|
313
540
|
|
|
314
541
|
return promise;
|
|
@@ -410,10 +637,19 @@ Value ChatDecryptFile(const CallbackInfo& args) {
|
|
|
410
637
|
|
|
411
638
|
Object Init(Env env, Object exports) {
|
|
412
639
|
haskell_init();
|
|
640
|
+
auto* receivers = new Receivers();
|
|
641
|
+
// Stopping all receivers before joining any bounds teardown by the longest in-flight receive.
|
|
642
|
+
env.AddCleanupHook([receivers]() {
|
|
643
|
+
for (auto& entry : *receivers) {
|
|
644
|
+
entry.second->RequestStop();
|
|
645
|
+
}
|
|
646
|
+
delete receivers;
|
|
647
|
+
});
|
|
413
648
|
exports.Set("chat_migrate_init", Function::New(env, ChatMigrateInit));
|
|
414
|
-
exports.Set("
|
|
649
|
+
exports.Set("chat_migrate_init_queue", Function::New(env, ChatMigrateInitQueue));
|
|
650
|
+
exports.Set("chat_close_store", Function::New(env, ChatCloseStore, "chat_close_store", receivers));
|
|
415
651
|
exports.Set("chat_send_cmd", Function::New(env, ChatSendCmd));
|
|
416
|
-
exports.Set("chat_recv_msg_wait", Function::New(env, ChatRecvMsgWait));
|
|
652
|
+
exports.Set("chat_recv_msg_wait", Function::New(env, ChatRecvMsgWait, "chat_recv_msg_wait", receivers));
|
|
417
653
|
exports.Set("chat_write_file", Function::New(env, ChatWriteFile));
|
|
418
654
|
exports.Set("chat_read_file", Function::New(env, ChatReadFile));
|
|
419
655
|
exports.Set("chat_encrypt_file", Function::New(env, ChatEncryptFile));
|
package/cpp/simplex.h
CHANGED
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
|
|
12
12
|
extern "C" void hs_init(int argc, char **argv[]);
|
|
13
13
|
extern "C" void hs_init_with_rtsopts(int * argc, char **argv[]);
|
|
14
|
+
extern "C" void hs_thread_done(void);
|
|
14
15
|
|
|
15
16
|
typedef long* chat_ctrl;
|
|
16
17
|
|
|
17
18
|
// the last parameter is used to return the pointer to chat controller
|
|
18
19
|
extern "C" char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl);
|
|
20
|
+
extern "C" char *chat_migrate_init_queue(const char *path, const char *key, const char *confirm, const int queueSize, chat_ctrl *ctrl);
|
|
19
21
|
extern "C" char *chat_close_store(chat_ctrl ctrl);
|
|
20
22
|
extern "C" char *chat_reopen_store(chat_ctrl ctrl);
|
|
21
23
|
extern "C" char *chat_send_cmd(chat_ctrl ctrl, const char *cmd);
|
package/dist/api.d.ts
CHANGED
|
@@ -37,7 +37,7 @@ export interface BotAddressSettings {
|
|
|
37
37
|
export declare const defaultBotAddressSettings: BotAddressSettings;
|
|
38
38
|
export type EventSubscriberFunc<K extends CEvt.Tag> = (event: ChatEvent & {
|
|
39
39
|
type: K;
|
|
40
|
-
}) => void | Promise<void>;
|
|
40
|
+
}, chat: ChatApi) => void | Promise<void>;
|
|
41
41
|
export type EventSubscribers = {
|
|
42
42
|
[K in CEvt.Tag]?: EventSubscriberFunc<K>;
|
|
43
43
|
};
|
|
@@ -76,23 +76,26 @@ export declare class ChatApi {
|
|
|
76
76
|
* Initializes the ChatApi.
|
|
77
77
|
* @param {DbConfig} db - Database configuration (sqlite or postgres).
|
|
78
78
|
* @param {core.MigrationConfirmation} [confirm=core.MigrationConfirmation.YesUp] - Migration confirmation mode.
|
|
79
|
+
* @param {number} [queueSize] - Size of internal queues, the core default is used when omitted.
|
|
79
80
|
*/
|
|
80
|
-
static init(db: DbConfig, confirm?: core.MigrationConfirmation): Promise<ChatApi>;
|
|
81
|
+
static init(db: DbConfig, confirm?: core.MigrationConfirmation, queueSize?: number): Promise<ChatApi>;
|
|
81
82
|
/**
|
|
82
83
|
* Start chat controller. Must be called with the existing user profile.
|
|
83
84
|
*/
|
|
84
85
|
startChat(): Promise<void>;
|
|
85
86
|
/**
|
|
86
87
|
* Stop chat controller.
|
|
87
|
-
*
|
|
88
|
+
* `close` calls it before closing the database.
|
|
88
89
|
* Usually doesn't need to be called in chat bots.
|
|
89
90
|
*/
|
|
90
91
|
stopChat(): Promise<void>;
|
|
91
92
|
/**
|
|
92
|
-
*
|
|
93
|
+
* Stop chat controller and close chat database.
|
|
94
|
+
* The database is not closed if stopping fails.
|
|
93
95
|
* Usually doesn't need to be called in chat bots.
|
|
94
96
|
*/
|
|
95
97
|
close(): Promise<void>;
|
|
98
|
+
private stopEventsLoop;
|
|
96
99
|
private runEventsLoop;
|
|
97
100
|
/**
|
|
98
101
|
* Subscribe multiple event handlers at once.
|
|
@@ -225,7 +228,7 @@ export declare class ChatApi {
|
|
|
225
228
|
* Add/remove message reaction.
|
|
226
229
|
* Network usage: background.
|
|
227
230
|
*/
|
|
228
|
-
apiChatItemReaction(chatType: T.ChatType, chatId: number, chatItemId: number, add: boolean, reaction: T.MsgReaction): Promise<T.
|
|
231
|
+
apiChatItemReaction(chatType: T.ChatType, chatId: number, chatItemId: number, add: boolean, reaction: T.MsgReaction): Promise<T.ACIReaction>;
|
|
229
232
|
/**
|
|
230
233
|
* Receive file.
|
|
231
234
|
* Network usage: no.
|
|
@@ -321,7 +324,7 @@ export declare class ChatApi {
|
|
|
321
324
|
* Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link
|
|
322
325
|
* Network usage: interactive.
|
|
323
326
|
*/
|
|
324
|
-
apiConnect(userId: number, incognito: boolean, preparedLink
|
|
327
|
+
apiConnect(userId: number, incognito: boolean, preparedLink: T.CreatedConnLink): Promise<ConnReqType>;
|
|
325
328
|
/**
|
|
326
329
|
* Connect via SimpleX link as string in the active user profile.
|
|
327
330
|
* Network usage: interactive.
|
package/dist/api.js
CHANGED
|
@@ -52,48 +52,62 @@ class ChatApi {
|
|
|
52
52
|
* Initializes the ChatApi.
|
|
53
53
|
* @param {DbConfig} db - Database configuration (sqlite or postgres).
|
|
54
54
|
* @param {core.MigrationConfirmation} [confirm=core.MigrationConfirmation.YesUp] - Migration confirmation mode.
|
|
55
|
+
* @param {number} [queueSize] - Size of internal queues, the core default is used when omitted.
|
|
55
56
|
*/
|
|
56
|
-
static async init(db, confirm = core.MigrationConfirmation.YesUp) {
|
|
57
|
+
static async init(db, confirm = core.MigrationConfirmation.YesUp, queueSize) {
|
|
57
58
|
const [path, key] = dbConfigToMigrateArgs(db);
|
|
58
|
-
const ctrl = await core.chatMigrateInit(path, key, confirm);
|
|
59
|
+
const ctrl = await core.chatMigrateInit(path, key, confirm, queueSize);
|
|
59
60
|
return new ChatApi(ctrl);
|
|
60
61
|
}
|
|
61
62
|
/**
|
|
62
63
|
* Start chat controller. Must be called with the existing user profile.
|
|
63
64
|
*/
|
|
64
65
|
async startChat() {
|
|
66
|
+
if (this.eventsLoop)
|
|
67
|
+
throw new Error("chat already started");
|
|
68
|
+
const ctrl = this.ctrl;
|
|
65
69
|
this.receiveEvents = true;
|
|
66
70
|
this.eventsLoop = this.runEventsLoop();
|
|
67
|
-
|
|
71
|
+
let r;
|
|
72
|
+
try {
|
|
73
|
+
r = await core.chatSendCmd(ctrl, types_1.CC.StartChat.cmdString({ mainApp: true, enableSndFiles: true, serviceRequests: false }));
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
await this.stopEventsLoop();
|
|
77
|
+
throw e;
|
|
78
|
+
}
|
|
68
79
|
if (r.type !== "chatStarted" && r.type !== "chatRunning") {
|
|
80
|
+
await this.stopEventsLoop();
|
|
69
81
|
throw new ChatCommandError("error starting chat", r);
|
|
70
82
|
}
|
|
71
83
|
}
|
|
72
84
|
/**
|
|
73
85
|
* Stop chat controller.
|
|
74
|
-
*
|
|
86
|
+
* `close` calls it before closing the database.
|
|
75
87
|
* Usually doesn't need to be called in chat bots.
|
|
76
88
|
*/
|
|
77
89
|
async stopChat() {
|
|
78
90
|
const r = await this.sendChatCmd("/_stop");
|
|
79
91
|
if (r.type !== "chatStopped")
|
|
80
|
-
throw new ChatCommandError("error
|
|
81
|
-
this.
|
|
82
|
-
if (this.eventsLoop)
|
|
83
|
-
await this.eventsLoop;
|
|
84
|
-
this.eventsLoop = undefined;
|
|
92
|
+
throw new ChatCommandError("error stopping chat", r);
|
|
93
|
+
await this.stopEventsLoop();
|
|
85
94
|
}
|
|
86
95
|
/**
|
|
87
|
-
*
|
|
96
|
+
* Stop chat controller and close chat database.
|
|
97
|
+
* The database is not closed if stopping fails.
|
|
88
98
|
* Usually doesn't need to be called in chat bots.
|
|
89
99
|
*/
|
|
90
100
|
async close() {
|
|
101
|
+
// a running controller keeps using the database connections that closing frees
|
|
102
|
+
await this.stopChat();
|
|
103
|
+
await core.chatCloseStore(this.ctrl);
|
|
104
|
+
this.ctrl_ = undefined;
|
|
105
|
+
}
|
|
106
|
+
async stopEventsLoop() {
|
|
91
107
|
this.receiveEvents = false;
|
|
92
108
|
if (this.eventsLoop)
|
|
93
109
|
await this.eventsLoop;
|
|
94
110
|
this.eventsLoop = undefined;
|
|
95
|
-
await core.chatCloseStore(this.ctrl);
|
|
96
|
-
this.ctrl_ = undefined;
|
|
97
111
|
}
|
|
98
112
|
async runEventsLoop() {
|
|
99
113
|
while (this.receiveEvents) {
|
|
@@ -105,7 +119,7 @@ class ChatApi {
|
|
|
105
119
|
if (subs) {
|
|
106
120
|
for (const { subscriber, once } of [...subs]) {
|
|
107
121
|
try {
|
|
108
|
-
const p = subscriber(event);
|
|
122
|
+
const p = subscriber(event, this);
|
|
109
123
|
if (p instanceof Promise)
|
|
110
124
|
await p;
|
|
111
125
|
}
|
|
@@ -118,7 +132,7 @@ class ChatApi {
|
|
|
118
132
|
}
|
|
119
133
|
for (const r of [...this.receivers]) {
|
|
120
134
|
try {
|
|
121
|
-
const p = r(event);
|
|
135
|
+
const p = r(event, this);
|
|
122
136
|
if (p instanceof Promise)
|
|
123
137
|
await p;
|
|
124
138
|
}
|
|
@@ -270,7 +284,7 @@ class ChatApi {
|
|
|
270
284
|
async sendChatCmd(cmd) {
|
|
271
285
|
return await core.chatSendCmd(this.ctrl, cmd);
|
|
272
286
|
}
|
|
273
|
-
async recvChatEvent(wait =
|
|
287
|
+
async recvChatEvent(wait = 500000) {
|
|
274
288
|
return await core.chatRecvMsgWait(this.ctrl, wait);
|
|
275
289
|
}
|
|
276
290
|
/**
|
|
@@ -322,8 +336,10 @@ class ChatApi {
|
|
|
322
336
|
switch (r.type) {
|
|
323
337
|
case "userProfileUpdated":
|
|
324
338
|
return r.updateSummary;
|
|
339
|
+
case "userProfileNoChange":
|
|
340
|
+
return { updateSuccesses: 0, updateFailures: 0, changedContacts: [] };
|
|
325
341
|
default:
|
|
326
|
-
throw new ChatCommandError("error
|
|
342
|
+
throw new ChatCommandError("error setting profile address", r);
|
|
327
343
|
}
|
|
328
344
|
}
|
|
329
345
|
/**
|
|
@@ -389,7 +405,7 @@ class ChatApi {
|
|
|
389
405
|
liveMessage,
|
|
390
406
|
updatedMessage: { msgContent, mentions: {} },
|
|
391
407
|
}));
|
|
392
|
-
if (r.type === "chatItemUpdated")
|
|
408
|
+
if (r.type === "chatItemUpdated" || r.type === "chatItemNotChanged")
|
|
393
409
|
return r.chatItem.chatItem;
|
|
394
410
|
throw new ChatCommandError("error updating chat item", r);
|
|
395
411
|
}
|
|
@@ -419,8 +435,8 @@ class ChatApi {
|
|
|
419
435
|
*/
|
|
420
436
|
async apiChatItemReaction(chatType, chatId, chatItemId, add, reaction) {
|
|
421
437
|
const r = await this.sendChatCmd(types_1.CC.APIChatItemReaction.cmdString({ chatRef: { chatType, chatId }, chatItemId, add, reaction }));
|
|
422
|
-
if (r.type === "
|
|
423
|
-
return r.
|
|
438
|
+
if (r.type === "chatItemReaction")
|
|
439
|
+
return r.reaction;
|
|
424
440
|
throw new ChatCommandError("error setting item reaction", r);
|
|
425
441
|
}
|
|
426
442
|
/**
|
|
@@ -431,6 +447,8 @@ class ChatApi {
|
|
|
431
447
|
const r = await this.sendChatCmd(types_1.CC.ReceiveFile.cmdString({ fileId, userApprovedRelays: true }));
|
|
432
448
|
if (r.type === "rcvFileAccepted")
|
|
433
449
|
return r.chatItem;
|
|
450
|
+
if (r.type === "rcvFileAcceptedSndCancelled")
|
|
451
|
+
throw new ChatCommandError("file cancelled by sender", r);
|
|
434
452
|
throw new ChatCommandError("error receiving file", r);
|
|
435
453
|
}
|
|
436
454
|
/**
|