# Использование 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++.