Files
Balance/BalanceAndroid/docs/SYNC.md
T

4.6 KiB
Raw Blame History

Серверная синхронизация

Android-клиент использует тот же self-hosted BalanceServer, что и остальные клиенты Balance.

API

POST endpoints:

/v1/auth/register
/v1/auth/login
/v1/auth/logout
/v1/auth/refresh
/v1/sync

Авторизованный запрос:

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

{
  "cursor": 0,
  "deviceId": "UUID",
  "changes": [],
  "pull": true
}

Push использует тот же endpoint с pull=false.

Change envelope

{
  "entity": "transaction",
  "id": "UUID",
  "deleted": false,
  "updatedAt": "2026-09-08T10:00:00Z",
  "payload": {}
}

Entities:

transaction
category
budget

Transaction payload

{
  "amount": 1000,
  "date": "2026-09-08T10:00:00Z",
  "note": "Кофе",
  "categoryName": "Кофе",
  "categoryIcon": "cup.and.saucer.fill",
  "categoryEmoji": "☕️",
  "categoryColorName": "brown",
  "isBalanceAdjustment": false,
  "kindRawValue": "expense"
}

Category payload

{
  "name": "Кофе",
  "icon": "cup.and.saucer.fill",
  "emoji": "☕️",
  "colorName": "brown",
  "kindRawValue": "expense",
  "createdAt": "2026-09-01T10:00:00Z"
}

Budget payload

{
  "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.

Сервер возвращает:

{
  "cursor": 42,
  "changes": [],
  "hasMore": false
}

Если hasMore=true, клиент повторяет pull с новым cursor.

Есть защитные проверки:

  • cursor не может уменьшиться;
  • hasMore=true должен сопровождаться продвижением cursor.

Applying remote changes

Изменения сортируются по sequence.

Для обычного изменения:

apply remote iff remote.updatedAt > local.updatedAt

Для удаления:

delete iff local.updatedAt <= remote.updatedAt

После применения соответствующий deletion journal очищается.

Scope

Cursor и lastPush привязаны к:

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 и документацию.