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
@@ -9,6 +9,7 @@ from __future__ import annotations
import asyncio
import ctypes
import json
from concurrent.futures import Executor
from enum import StrEnum
from typing import Any, TypedDict
@@ -102,7 +103,9 @@ async def chat_send_cmd(ctrl: int, cmd: str) -> CR.ChatResponse:
raise ChatAPIError(f"invalid chat command result: {raw[:200]}")
async def chat_recv_msg_wait(ctrl: int, wait_us: int = 500_000) -> CEvt.ChatEvent | None:
async def chat_recv_msg_wait(
ctrl: int, wait_us: int = 500_000, executor: Executor | None = None
) -> CEvt.ChatEvent | None:
def _call() -> str:
# On timeout, the C side returns a non-NULL pointer to a single NUL byte
# (see Mobile.hs `fromMaybe ""`), so `_read_and_free` returns "" — no
@@ -110,7 +113,10 @@ async def chat_recv_msg_wait(ctrl: int, wait_us: int = 500_000) -> CEvt.ChatEven
ptr = _native.lib().chat_recv_msg_wait(ctrl, wait_us)
return _read_and_free(ptr)
raw = await asyncio.to_thread(_call)
if executor is None:
raw = await asyncio.to_thread(_call)
else:
raw = await asyncio.get_running_loop().run_in_executor(executor, _call)
if not raw:
return None
parsed = json.loads(raw)
@@ -122,17 +128,29 @@ async def chat_recv_msg_wait(ctrl: int, wait_us: int = 500_000) -> CEvt.ChatEven
raise ChatAPIError(f"invalid chat event: {raw[:200]}")
async def chat_migrate_init(db_path: str, db_key: str, confirm: MigrationConfirmation) -> int:
"""Initialize chat controller. Returns opaque ctrl pointer as Python int."""
async def chat_migrate_init(
db_path: str,
db_key: str,
confirm: MigrationConfirmation,
queue_size: int | None = None,
) -> int:
"""Initialize chat controller. Returns opaque ctrl pointer as Python int.
`queue_size` is the size of internal queues; the core default is used when None.
"""
# ctypes silently wraps ints that do not fit C int.
if queue_size is not None and ctypes.c_int(queue_size).value != queue_size:
raise ValueError(f"queue_size {queue_size} does not fit C int")
init_queue = _native.migrate_init_queue() if queue_size is not None else None
def _call() -> tuple[int, str]:
ctrl = ctypes.c_void_p()
ptr = _native.lib().chat_migrate_init(
db_path.encode("utf-8"),
db_key.encode("utf-8"),
confirm.encode("utf-8"),
ctypes.byref(ctrl),
)
args = (db_path.encode("utf-8"), db_key.encode("utf-8"), confirm.encode("utf-8"))
if init_queue is None:
ptr = _native.lib().chat_migrate_init(*args, ctypes.byref(ctrl))
else:
ptr = init_queue(*args, queue_size, ctypes.byref(ctrl))
return (ctrl.value or 0, _read_and_free(ptr))
ctrl_val, raw = await asyncio.to_thread(_call)