core, libs: configurable queue size, library fixes (#7542)

* core: add chat_migrate_init_queue FFI export

* bots: fix BadgeServiceErrorCode API type

* nodejs: pass required command fields

* nodejs: fix migration error types

* nodejs: install libsimplex from SIMPLEX_LIBS_DIR

* nodejs: add queue size option

* nodejs: regenerate docs

* python: add queue size option

* bots: pass incognito in APIConnect

* nodejs: accept documented success responses

* python: accept documented success responses

* nodejs: dispatch each bot message once

* nodejs: fix startChat events loop lifecycle

* nodejs, python: parse multi-line bot commands

* nodejs: fix file buffer handling in addon

* nodejs: keep events loop when chat stop fails

* python: fix send_and_wait race, load lib off loop

* python: make queue size export optional

* nodejs: receive events on a dedicated thread

* nodejs: release haskell thread after receive

* python: receive on a dedicated thread per chat

* python: test receive shutdown order

* nodejs: one receive thread per chat controller

* nodejs: harden receiver shutdown and tests

* nodejs: stop chat before closing store

* python: stop chat before closing store

* nodejs, python: harden close regression and retry

* python: free results with ucrt on windows

* nodejs: enable c++ exceptions on mac and windows
This commit is contained in:
sh
2026-09-19 12:18:46 +01:00
committed by GitHub
parent ecb9d87157
commit c8f20bcc91
78 changed files with 1670 additions and 571 deletions
@@ -2,7 +2,9 @@
from __future__ import annotations
import asyncio
import json
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from typing import Any, Literal
@@ -65,17 +67,20 @@ class ChatApi:
def __init__(self, ctrl: int):
self._ctrl: int | None = ctrl
self._started = False
self._recv_executor: ThreadPoolExecutor | None = None
@classmethod
async def init(
cls,
db: Db,
confirm: MigrationConfirmation = MigrationConfirmation.YES_UP,
queue_size: int | None = None,
) -> ChatApi:
path_or_prefix, key_or_conn, backend = _db_to_migrate_args(db)
# Trigger lazy lib load with the right backend BEFORE chat_migrate_init.
_native.lib_for(backend)
ctrl = await core.chat_migrate_init(path_or_prefix, key_or_conn, confirm)
# It may download ~100 MB, so it must not block the event loop.
await asyncio.to_thread(_native.lib_for, backend)
ctrl = await core.chat_migrate_init(path_or_prefix, key_or_conn, confirm, queue_size)
return cls(ctrl)
@property
@@ -114,6 +119,14 @@ class ChatApi:
self._started = False
async def close(self) -> None:
"""Stop the chat and close its store; the store stays open if stopping fails."""
# a running controller keeps using the database connections that closing frees
await self.stop_chat()
if self._recv_executor is not None:
# Waits for a receive already in flight (up to wait_us) so the store
# never closes underneath one; run off-loop since shutdown blocks.
await asyncio.to_thread(self._recv_executor.shutdown, wait=True)
self._recv_executor = None
await core.chat_close_store(self.ctrl)
self._ctrl = None
self._started = False
@@ -122,7 +135,14 @@ class ChatApi:
return await core.chat_send_cmd(self.ctrl, cmd)
async def recv_chat_event(self, wait_us: int = 500_000) -> CEvt.ChatEvent | None:
return await core.chat_recv_msg_wait(self.ctrl, wait_us)
ctrl = self.ctrl # raises before touching the executor if close() was called
if self._recv_executor is None:
# A receive blocks for up to wait_us almost back to back, so it would
# otherwise pin one of the default executor's few worker threads.
self._recv_executor = ThreadPoolExecutor(
max_workers=1, thread_name_prefix="simplex-recv"
)
return await core.chat_recv_msg_wait(ctrl, wait_us, self._recv_executor)
# ------------------------------------------------------------------ #
# Address commands
@@ -158,6 +178,8 @@ class ChatApi:
)
if r["type"] == "userProfileUpdated":
return r["updateSummary"]
if r["type"] == "userProfileNoChange":
return {"updateSuccesses": 0, "updateFailures": 0, "changedContacts": []}
raise ChatCommandError("error setting profile address", r)
async def api_set_address_settings(self, user_id: int, settings: T.AddressSettings) -> None:
@@ -236,6 +258,8 @@ class ChatApi:
)
if r["type"] == "chatItemUpdated":
return r["chatItem"]["chatItem"]
if r["type"] == "chatItemNotChanged":
return r["chatItem"]["chatItem"]
raise ChatCommandError("error updating chat item", r)
async def api_delete_chat_items(
@@ -302,6 +326,8 @@ class ChatApi:
)
if r["type"] == "rcvFileAccepted":
return r["chatItem"]
if r["type"] == "rcvFileAcceptedSndCancelled":
raise ChatCommandError("file cancelled by sender", r)
raise ChatCommandError("error receiving file", r)
async def api_cancel_file(self, file_id: int) -> None:
@@ -477,12 +503,13 @@ class ChatApi:
self,
user_id: int,
incognito: bool,
prepared_link: T.CreatedConnLink | None = None,
prepared_link: T.CreatedConnLink,
) -> ConnReqType:
args: CC.APIConnect = {"userId": user_id, "incognito": incognito}
if prepared_link is not None:
args["preparedLink_"] = prepared_link
r = await self.send_chat_cmd(CC.APIConnect_cmd_string(args))
r = await self.send_chat_cmd(
CC.APIConnect_cmd_string(
{"userId": user_id, "incognito": incognito, "preparedLink_": prepared_link}
)
)
return self._handle_connect_result(r)
async def api_connect_active_user(self, conn_link: str) -> ConnReqType: