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.
Files changed (64) hide show
  1. package/README.md +4 -2
  2. package/binding.gyp +2 -0
  3. package/cpp/simplex.cc +265 -29
  4. package/cpp/simplex.h +2 -0
  5. package/dist/api.d.ts +9 -6
  6. package/dist/api.js +37 -19
  7. package/dist/api.js.map +1 -1
  8. package/dist/bot.d.ts +6 -2
  9. package/dist/bot.js +17 -34
  10. package/dist/bot.js.map +1 -1
  11. package/dist/core.d.ts +11 -7
  12. package/dist/core.js +5 -2
  13. package/dist/core.js.map +1 -1
  14. package/dist/simplex.d.ts +3 -2
  15. package/dist/util.js +1 -1
  16. package/dist/util.js.map +1 -1
  17. package/docs/Namespace.bot.md +1 -0
  18. package/docs/api.Class.ChatApi.md +122 -78
  19. package/docs/api.TypeAlias.EventSubscriberFunc.md +5 -1
  20. package/docs/bot.Function.run.md +1 -1
  21. package/docs/bot.Function.subscribeChatItems.md +27 -0
  22. package/docs/bot.Interface.BotConfig.md +13 -9
  23. package/docs/bot.Interface.BotOptions.md +10 -10
  24. package/docs/bot.TypeAlias.BotDbOpts.md +4 -0
  25. package/docs/core.Class.ChatAPIError.md +4 -4
  26. package/docs/core.Class.ChatInitError.md +4 -4
  27. package/docs/core.DBMigrationError.Interface.ErrorMigration.md +4 -4
  28. package/docs/core.DBMigrationError.Interface.ErrorNotADatabase.md +3 -3
  29. package/docs/core.DBMigrationError.Interface.ErrorSQL.md +4 -4
  30. package/docs/core.DBMigrationError.Interface.InvalidConfirmation.md +2 -2
  31. package/docs/core.DBMigrationError.Interface.InvalidQueueSize.md +25 -0
  32. package/docs/core.DBMigrationError.TypeAlias.Tag.md +2 -2
  33. package/docs/core.Enumeration.MigrationConfirmation.md +5 -5
  34. package/docs/core.Function.chatCloseStore.md +1 -1
  35. package/docs/core.Function.chatDecryptFile.md +1 -1
  36. package/docs/core.Function.chatEncryptFile.md +1 -1
  37. package/docs/core.Function.chatMigrateInit.md +8 -2
  38. package/docs/core.Function.chatReadFile.md +3 -3
  39. package/docs/core.Function.chatRecvMsgWait.md +1 -1
  40. package/docs/core.Function.chatSendCmd.md +1 -1
  41. package/docs/core.Function.chatWriteFile.md +2 -2
  42. package/docs/core.Interface.APIResult.md +3 -3
  43. package/docs/core.Interface.CryptoArgs.md +3 -3
  44. package/docs/core.Interface.UpMigration.md +3 -3
  45. package/docs/core.MTRError.Interface.MTREDifferent.md +3 -3
  46. package/docs/core.MTRError.Interface.MTRENoDown.md +10 -10
  47. package/docs/core.MTRError.TypeAlias.Tag.md +1 -1
  48. package/docs/core.MigrationError.Interface.MEDowngrade.md +3 -3
  49. package/docs/core.MigrationError.Interface.MEUpgrade.md +4 -4
  50. package/docs/core.MigrationError.Interface.MigrationError.md +3 -3
  51. package/docs/core.MigrationError.TypeAlias.Tag.md +1 -1
  52. package/docs/core.Namespace.DBMigrationError.md +1 -0
  53. package/docs/core.TypeAlias.DBMigrationError.md +2 -2
  54. package/docs/core.TypeAlias.MTRError.md +1 -1
  55. package/docs/core.TypeAlias.MigrationError.md +1 -1
  56. package/examples/squaring-bot-readme.js +2 -2
  57. package/examples/squaring-bot.ts +4 -4
  58. package/package.json +2 -2
  59. package/src/api.ts +54 -25
  60. package/src/bot.ts +43 -37
  61. package/src/core.ts +15 -7
  62. package/src/download-libs.js +28 -1
  63. package/src/simplex.d.ts +3 -2
  64. 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
- const [chat, _user, _address] = await bot.run({
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
- if (original_buf == nullptr || binary_len == 0) {
107
- Callback().Call({Env().Null(), Env().Undefined()});
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
- char* data_ptr = original_buf + 5;
111
- auto finalizer = [](Napi::Env env, char* finalize_data, char* orig) {
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
- Function cb;
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
- ArrayBuffer ab = args[2].As<ArrayBuffer>();
299
- char* data = static_cast<char*>(ab.Data());
300
- size_t len = ab.ByteLength();
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, ab, data, len](ResultAsyncWorker* worker) {
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("chat_close_store", Function::New(env, ChatCloseStore));
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
- * Must be called before closing the database.
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
- * Close chat database.
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.ChatItemDeletion[]>;
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?: T.CreatedConnLink): Promise<ConnReqType>;
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
- const r = await this.sendChatCmd(types_1.CC.StartChat.cmdString({ mainApp: true, enableSndFiles: true, serviceRequests: false }));
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
- * Must be called before closing the database.
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 starting chat", r);
81
- this.receiveEvents = false;
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
- * Close chat database.
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 = 5000000) {
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 loading user address", r);
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 === "chatItemsDeleted")
423
- return r.chatItemDeletions;
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
  /**