213 lines
4.6 KiB
Markdown
213 lines
4.6 KiB
Markdown
# Серверная синхронизация
|
||
|
||
Android-клиент использует тот же self-hosted BalanceServer, что и остальные клиенты Balance.
|
||
|
||
## API
|
||
|
||
POST endpoints:
|
||
|
||
```text
|
||
/v1/auth/register
|
||
/v1/auth/login
|
||
/v1/auth/logout
|
||
/v1/auth/refresh
|
||
/v1/sync
|
||
```
|
||
|
||
Авторизованный запрос:
|
||
|
||
```http
|
||
Authorization: Bearer <access-token>
|
||
Content-Type: application/json; charset=utf-8
|
||
```
|
||
|
||
## Session
|
||
|
||
`ServerSession` содержит:
|
||
|
||
- access token;
|
||
- refresh token;
|
||
- expiresAt;
|
||
- userId;
|
||
- email.
|
||
|
||
`SecureSessionStore` сериализует session JSON и шифрует его AES/GCM.
|
||
|
||
AES key создаётся и хранится в Android Keystore.
|
||
|
||
При ошибке расшифровки store очищается.
|
||
|
||
## Server URL
|
||
|
||
`ServerPreferences.normalizeServer` принимает полный URL без:
|
||
- user info;
|
||
- query;
|
||
- fragment.
|
||
|
||
Разрешены HTTPS и локальный HTTP.
|
||
|
||
HTTP допускается для:
|
||
- `localhost`
|
||
- `127.0.0.1`
|
||
- `10.0.2.2`
|
||
|
||
`10.0.2.2` — специальный адрес Android Emulator для host machine.
|
||
|
||
## Authentication
|
||
|
||
Перед авторизованным запросом клиент проверяет expiry.
|
||
|
||
Если осталось менее 60 секунд, выполняется refresh.
|
||
|
||
При HTTP 401 клиент один раз refresh-ит session и повторяет исходный запрос.
|
||
|
||
Если refresh не удался, session очищается.
|
||
|
||
## Sync request
|
||
|
||
```json
|
||
{
|
||
"cursor": 0,
|
||
"deviceId": "UUID",
|
||
"changes": [],
|
||
"pull": true
|
||
}
|
||
```
|
||
|
||
Push использует тот же endpoint с `pull=false`.
|
||
|
||
## Change envelope
|
||
|
||
```json
|
||
{
|
||
"entity": "transaction",
|
||
"id": "UUID",
|
||
"deleted": false,
|
||
"updatedAt": "2026-09-08T10:00:00Z",
|
||
"payload": {}
|
||
}
|
||
```
|
||
|
||
Entities:
|
||
|
||
```text
|
||
transaction
|
||
category
|
||
budget
|
||
```
|
||
|
||
## Transaction payload
|
||
|
||
```json
|
||
{
|
||
"amount": 1000,
|
||
"date": "2026-09-08T10:00:00Z",
|
||
"note": "Кофе",
|
||
"categoryName": "Кофе",
|
||
"categoryIcon": "cup.and.saucer.fill",
|
||
"categoryEmoji": "☕️",
|
||
"categoryColorName": "brown",
|
||
"isBalanceAdjustment": false,
|
||
"kindRawValue": "expense"
|
||
}
|
||
```
|
||
|
||
## Category payload
|
||
|
||
```json
|
||
{
|
||
"name": "Кофе",
|
||
"icon": "cup.and.saucer.fill",
|
||
"emoji": "☕️",
|
||
"colorName": "brown",
|
||
"kindRawValue": "expense",
|
||
"createdAt": "2026-09-01T10:00:00Z"
|
||
}
|
||
```
|
||
|
||
## Budget payload
|
||
|
||
```json
|
||
{
|
||
"categoryName": "Кофе",
|
||
"categoryIcon": "cup.and.saucer.fill",
|
||
"categoryEmoji": "☕️",
|
||
"limit": 5000,
|
||
"monthStart": "2026-09-01T00:00:00Z"
|
||
}
|
||
```
|
||
|
||
## Push
|
||
|
||
Изменения выбираются по `updatedAt` между `lastPush` и cutoff.
|
||
|
||
Batch size: 100.
|
||
|
||
Если JSON batch больше 1.5 MB, он рекурсивно делится.
|
||
|
||
## Pull
|
||
|
||
Клиент отправляет текущий cursor.
|
||
|
||
Сервер возвращает:
|
||
|
||
```json
|
||
{
|
||
"cursor": 42,
|
||
"changes": [],
|
||
"hasMore": false
|
||
}
|
||
```
|
||
|
||
Если `hasMore=true`, клиент повторяет pull с новым cursor.
|
||
|
||
Есть защитные проверки:
|
||
- cursor не может уменьшиться;
|
||
- `hasMore=true` должен сопровождаться продвижением cursor.
|
||
|
||
## Applying remote changes
|
||
|
||
Изменения сортируются по `sequence`.
|
||
|
||
Для обычного изменения:
|
||
|
||
```text
|
||
apply remote iff remote.updatedAt > local.updatedAt
|
||
```
|
||
|
||
Для удаления:
|
||
|
||
```text
|
||
delete iff local.updatedAt <= remote.updatedAt
|
||
```
|
||
|
||
После применения соответствующий deletion journal очищается.
|
||
|
||
## Scope
|
||
|
||
Cursor и lastPush привязаны к:
|
||
|
||
```text
|
||
serverUrl + userId
|
||
```
|
||
|
||
Это позволяет иметь независимое состояние sync для разных серверов/аккаунтов.
|
||
|
||
## Reset
|
||
|
||
`resetAndSync()` очищает cursor и lastPush текущего scope, затем запускает обычную sync.
|
||
|
||
Локальные финансовые данные при этом не удаляются.
|
||
|
||
## Background
|
||
|
||
`SyncWorker`:
|
||
- запускается WorkManager;
|
||
- требует network connection;
|
||
- пропускает работу без session/server URL;
|
||
- возвращает `retry`, если sync завершилась ошибкой.
|
||
|
||
## Совместимость
|
||
|
||
README проекта указывает совместимость Android-клиента с BalanceServer protocol 3.1. При изменении контракта сервера необходимо синхронно обновлять `ServerApi`, `SyncRepository` и документацию.
|