liner_broker 1.3.0

Redis based message serverless broker.
Documentation
# Использование liner с SQLite

Руководство для интеграторов, которые запускают брокер с **файлом SQLite** вместо Redis: как создавать клиентов, когда нужен **`receivers_json`**, как **сидированные ключи по проводу** связаны с маршрутизацией и как повторить **эталонный модульный тест** в своём коде.

**Также прочитайте:** [backends.md](backends.md) (выбор бэкенда, модель изолированной БД), [operations-redis-sqlite.md](operations-redis-sqlite.md) (WAL, бэкап, `clear_*`), [errors-and-logging.md](errors-and-logging.md) (`NULL` / `None`, stderr), [bindings.md](bindings.md) (C / Python поверх `include/liner.h`).

---

## Когда уместен SQLite

- **Один файл**, без сервера Redis — встроенные тесты, ноутбуки, устройства.
- **Тот же API обмена**, что у Redis: `run`, `send_to`, `subscribe`, TCP между пирами.
- **Каталог** (какой топик слушает на каком адресе и какой целочисленный **ключ топика** по проводу) хранится в таблицах SQLite `topic_addr` и `topic_key` (см. [routing-and-store-layout.md](routing-and-store-layout.md)).

**Оговорка:** если **у каждого процесса свой файл `.sqlite`**, каталоги **не** общие. Пустая БД sender’а не знает топик пира, пока вы **не засидируете** каталог (`receivers_json`, предыдущий запуск на том же файле или ручной SQL). Redis избегает этого за счёт одного общего URL.

**Изолированные файлы и `at_least_once_delivery`:** ack listener пишутся в **файл того процесса** (`conn_mess_number`), а sender обновляет ack из **своего** файла. При **разных путях БД у пиров** эти представления **не** сливаются — держите **`at_least_once_delivery` = false** (`lnr_send_to` / `send_to` / `send_all` последний аргумент, в Rust `false`), чтобы стек не держал неподтверждённый at-least-once трафик в RAM в ожидании обновления хранилища, которое никогда не придёт. Для настоящего at-least-once между процессами используйте **один общий путь SQLite** (как один URL Redis) или Redis.

**Изолированная пара (пустые БД):** первый логический канал использует по проводу **`connection_key` = 1** (в этой модели документируется как **token id**). `receivers_json` **не** несёт этот id; перечисляйте **только пиров** (их `topic` / `addr` / `client_name`). Сидирование вставляет **`conn_sender(1, peer_topic)`** и **`conn_key_map`** для этих строк, и **`INSERT OR IGNORE topic_key(your_source_topic, 1)`**, чтобы у listener’а был проволочный ключ без самострочной строки в JSON. **По проводу `topic_key.k = 1`** и для каждого засидированного топика пира (не передаётся в JSON).

**Один общий файл SQLite** (один путь для согласующихся процессов): передавайте **`receivers_json` пустым** (`""` / `[]`); пиры регистрируются в одном хранилище, `conn_sender` и ключи остаются согласованными без JSON-каталога.

---

## Создание клиента

### Rust

Используйте **`liner_broker::Client`** или **`liner_broker::Liner`**; оба принимают **`at_least_once_delivery`** в **`send_to`** / **`send_all`**. В README крейта по-прежнему показан **`Liner::new`** для Redis.

**`Liner::new_sqlite`** принимает те же пять логических аргументов, что C API (`receivers_json` может быть `""`).

```rust
use liner_broker::Liner;

let mut c = Liner::new_sqlite(
    "my_unique_name",
    "my_source_topic",
    "127.0.0.1:0",
    "/path/to/db.sqlite",
    "", // или JSON-строка каталога для изолированных БД
);
c.clear_stored_messages();
c.clear_addresses_of_topic();
assert!(c.run(Box::new(|_to, _from, _data| { /* … */ })));
```

После успешного **`run`** у **`Liner`** можно вызывать:

- **`bound_listen_addr()`** → `Option<String>` — реальный адрес привязки, если использовали порт **`0`** (для экспорта в каталог).
- **`unique_name()`** → `String` — значение, которое пиры должны указать в JSON в поле **`client_name`**.

Те же аксессоры есть у **`Client`** (`bound_listen_addr` / `unique_name` возвращают `Option<&str>` / `&str`).

### C (`include/liner.h`)

```c
lnr_hClient c = lnr_new_client_sqlite(
    "my_unique_name",
    "my_source_topic",
    "127.0.0.1:0",
    "/path/to/db.sqlite",
    NULL   /* или "[]" или UTF-8 строка JSON-массива */
);
if (!c) { /* неверные аргументы, ошибка открытия или некорректный непустой JSON */ }

BOOL ok = lnr_run(c, my_receive_cb, my_udata);
```

- **`receivers_json == NULL`**, пустая строка, только пробелы или JSON **`[]`** — **не** ошибка; сидирования нет (в БД уже могут быть строки от прошлой сессии).
- Некорректный **непустой** JSON → клиент **`NULL`**, сообщение в stderr.

---

## Формат каталога `receivers_json`

UTF-8 JSON: **один массив** объектов. У каждого объекта должны быть три поля:

| Поле | Смысл |
|------|--------|
| **`topic`** | **Зарегистрированное имя топика пира** (его `source_topic` / то, что вы передаёте в `send_to`, чтобы достучаться до него). Это же строка **`from`**, когда этот пир шлёт вам. (Устаревший вариант: строка, равная **вашему** `source_topic`, игнорируется для `conn_sender`; ваш проволочный `topic_key` обеспечивается сидером.) |
| **`addr`** | Удалённый TCP-адрес listener’а, например `127.0.0.1:54321` — должен совпадать с тем, как пир привязался (после `run` часто берут **`bound_listen_addr()`**, если использовали порт `0`). |
| **`client_name`** | Удалённый **`unique_name`** (что listener зарегистрировал в `topic_addr`). |

**Не в JSON:** проволочные **`topic_key`** и **`connection_key`** на канал — для изолированных пустых БД реализация сидирует **`topic_key.k = 1`** для каждого топика из каталога пира, то же для **вашего** `source_topic`, и **`conn_sender`** / **`conn_key_map`** для первого канала, как описано выше.

Повтор `(topic, addr)` или тот же **`topic`** с новой строкой: сидирование **upsert** (последняя строка побеждает при дубликатах топика в одном батче). См. [backends.md](backends.md) (*Изолированный SQLite*). Устаревшее свойство **`topic_key`** в объектах JSON парсер **игнорирует**.

Пример (один пир):

```json
[
  {
    "topic": "peer_topic",
    "addr": "127.0.0.1:2256",
    "client_name": "peer_unique"
  }
]
```

**Замечание по Redis:** `Store::seed_receivers` на бэкенде Redis — **no-op**; этот путь JSON для **изолированных** файлов SQLite.

---

## Эталонный разбор (те же шаги, что в модульном тесте)

В репозитории есть сквозной тест с **двумя разными файлами SQLite**, **файлом каталога JSON** и **`send_to`**. Используйте его как канонический рецепт.

- **Место:** [`src/client.rs`](../../src/client.rs), тест **`isolated_sqlite_two_clients_via_receivers_json_catalog_file`**.
- **Запуск:** `cargo test isolated_sqlite_two_clients_via_receivers_json_catalog_file`

**Шаги (как в тесте):**

1. **Приёмник (клиент A)**  
   - Отдельный путь к БД, например `…/a.sqlite`.  
   - `Client::new_sqlite("unique_a_iso", "topic_iso_a", "127.0.0.1:0", path_a, "")` — пустой `receivers_json`.  
   - `run` с колбэком, который распознаёт отправляемый payload (в тесте проверяют `b"ping"`).

2. **Экспорт каталога после запуска A**  
   - `listen = client_a.bound_listen_addr().expect("…")` — реальный host:port после bind.  
   - Собрать JSON: один объект с `"topic": "topic_iso_a"`, `"addr": listen`, `"client_name": client_a.unique_name()` (без `topic_key`; сидирование назначает проволочный ключ **1**).  
   - Записать строку в файл (в тесте `catalog.json` во временной директории).

3. **Отправитель (клиент B)**  
   - Другой путь БД `…/b.sqlite`, другой **`unique_name`**, другой исходный топик (например `"topic_iso_b"`).  
   - Прочитать файл в строку и вызвать `Client::new_sqlite(..., &catalog_json)`.  
   - `run` (в тесте для B — no-op колбэк приёма).

4. **Отправка**  
   - B вызывает `send_to("topic_iso_a", payload, false)` в цикле повторов, пока не вернёт `true` (TCP и маршрутизация могут потребовать несколько попыток). Здесь **`false`**, потому что у A и B **разные** файлы SQLite; **`true`** ждал бы ack в БД B, которые обновляет только listener A (см. *Изолированные файлы и `at_least_once_delivery`* выше).

5. **Проверка доставки**  
   - Колбэк A должен увидеть payload в вашем таймауте.

Тест использует `std::env::temp_dir()` для путей и в конце удаляет директорию; в приложении — реальные пути или конфигурация.

---

## Общий файл SQLite и изолированные файлы

| Модель | Как разделяется каталог |
|--------|-------------------------|
| **Один файл `.sqlite`**, открытый согласующимися процессами (один хост, дисциплина блокировок) | `regist_topic` / `run` обновляют общие `topic_addr` / `topic_key`; пиры видят друг друга без JSON, если разделяют файл. Предпочтительно **пустой** `receivers_json`. |
| **Один файл на процесс** (изолированные пустые БД, один контрагент) | Используйте **`receivers_json`**, чтобы в каждой БД были **`topic`**, **`addr`**, **`client_name`** пира. Первый проволочный **`connection_key`** — **1**; засидированный **`topic_key.k`** — **1** для этих топиков; сидирование пишет **`conn_sender`** для колбэка **`from`**. Для **`send_to` / `send_all`** ставьте **`at_least_once_delivery == false`**, если только не используете **общий** путь SQLite (см. *Изолированные файлы и `at_least_once_delivery`* выше). |

**Не** смешивайте **Redis** и **SQLite** для одной логической сети, если не хотите двух изолированных систем ([backends.md](backends.md)).

---

## См. также

- [using-the-api.md](using-the-api.md) — порядок `run`, потоки, ловушки `send_to`.  
- [c-api-compatibility-and-build.md](c-api-compatibility-and-build.md) — сборка `libliner_broker` и линковка C/C++.