Files
BalanceServer/docs/SYNC.md
T
wtandClaude Opus 5.5 dfb4fe4e1a Implement ecosystem contract v2: capabilities, shared ledgers, attachments
- GET /v1/capabilities and the extended sync entity allowlist (§0, §1).
- Shared ledgers (§7): ledgers, members and hashed one-time invite codes;
  X-Balance-Ledger selects the space on /v1/sync and /v1/attachments with
  its own records, change stream and cursor. Personal space keeps the v1
  tables and behaviour; the migration only adds tables.
- Attachments (§8): SHA-256, size and media type checks, blob files in
  BALANCE_ATTACHMENTS_DIR (default: attachments next to the database),
  garbage collection of unreferenced blobs with a grace period for fresh
  uploads, and ledger deletion removes its blobs.
- Tests for every new endpoint and error code; API, sync, data model,
  security, deployment and README docs updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 23:30:12 +07:00

6.0 KiB
Raw Permalink Blame History

Синхронизация

Цель

Один аккаунт Balance может использоваться на нескольких устройствах:

iPhone
   │
   ├── transaction A
   ├── category B
   └── budget C
        │
        ▼
   BalanceServer
        │
   ┌────┴────┐
   ▼         ▼
 Android    Mac

Cursor

Каждый клиент хранит cursor последнего успешно применённого server change.

Принцип:

cursor = 100

server:
101 A
102 B
103 C

pull(cursor=100)
→ A, B, C
→ new cursor = 103

Клиент должен сохранять cursor только после успешного применения соответствующего batch.

Sequence

Server sequence задаёт глобальный порядок изменений в sync stream.

Требования:

  • монотонность;
  • отсутствие уменьшения;
  • однозначность порядка;
  • корректность при concurrent requests.

Push

Push передаёт изменения клиента:

local mutations
      ↓
POST /v1/sync
      ↓
validation
      ↓
ownership check
      ↓
persist
      ↓
append to change stream

Retry не должен создавать две разные логические записи из одного client change.

Pull

Pull получает изменения после cursor.

Сервер не должен возвращать изменения другого пользователя.

При pagination клиент должен иметь возможность продолжить с нового cursor.

Conflict resolution

Клиенты Balance используют timestamp-based versioning.

Типовая проверка:

local.updatedAt < remote.updatedAt

Следовательно, server должен сохранять updatedAt достаточно точно и не подменять его произвольным временем каждого чтения.

При конкурентных writes сервер должен иметь однозначное правило победителя.

Deletions

Удаление создаёт tombstone/change:

{
  "entity": "transaction",
  "id": "...",
  "deleted": true,
  "updatedAt": "..."
}

Tombstone должен попасть в change stream так же, как обычное изменение.

Idempotency

Push может повториться из-за:

  • timeout;
  • network failure;
  • app restart;
  • retry after 401/token refresh.

Поэтому сервер должен обрабатывать повторную отправку безопасно.

Batch

Apple-клиент отправляет batches до 100 изменений и может делить крупные payloads.

Android должен использовать совместимый protocol.

Server не должен предполагать, что все изменения приходят одним запросом.

Reset/full sync

Если client потерял cursor или требует reset, сервер должен поддерживать предусмотренный текущим API механизм полного pull либо новый cursor baseline.

Нельзя молча трактовать неизвестный/просроченный cursor как актуальный.

Пространства (контракт v2)

POST /v1/sync работает в одном пространстве данных:

  • без X-Balance-Ledger — личное пространство пользователя, таблицы records/changes, поведение v1 без изменений;
  • с X-Balance-Ledger: <id> — общее пространство, таблицы ledger_records/ledger_changes. Доступ проверяется на каждый запрос.

У общего пространства свой поток изменений: sequence берётся из отдельного AUTOINCREMENT ledger_changes, а pull фильтрует по ledger_id. Курсор личного пространства и курсор каждого ledger независимы, клиент хранит их раздельно. Все участники пишут в одно пространство с тем же last-write-wins по updatedAt; идемпотентность, пагинация (500), лимиты и транзакционность те же.

Удаление пространства физически удаляет его записи и историю изменений. Удалённый участник теряет доступ (403), записи остаются в пространстве.

Вложения и сборка мусора

Байты вложений загружаются отдельно (PUT /v1/attachments/{sha256}) до записи attachment в sync. Файл пространства считается мусором, когда ни одна живая (deleted=0) запись attachment этого пространства не ссылается на его sha256 (сравнение без учёта регистра). Сборка выполняется в той же транзакции, что и sync-запрос, содержащий изменения attachment, и при каждом PUT. Файл, ни разу не получивший ссылки, хранится не меньше 24 часов с последней загрузки, чтобы не удалить байты, чья запись ещё не синхронизирована; повторная загрузка перезапускает этот срок.

Security

Sync endpoint всегда ограничен текущим authenticated user.

deviceId — идентификатор устройства, а не механизм авторизации.