- 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>
168 lines
6.0 KiB
Markdown
168 lines
6.0 KiB
Markdown
# Синхронизация
|
||
|
||
## Цель
|
||
|
||
Один аккаунт Balance может использоваться на нескольких устройствах:
|
||
|
||
```text
|
||
iPhone
|
||
│
|
||
├── transaction A
|
||
├── category B
|
||
└── budget C
|
||
│
|
||
▼
|
||
BalanceServer
|
||
│
|
||
┌────┴────┐
|
||
▼ ▼
|
||
Android Mac
|
||
```
|
||
|
||
## Cursor
|
||
|
||
Каждый клиент хранит cursor последнего успешно применённого server change.
|
||
|
||
Принцип:
|
||
|
||
```text
|
||
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 передаёт изменения клиента:
|
||
|
||
```text
|
||
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.
|
||
|
||
Типовая проверка:
|
||
|
||
```text
|
||
local.updatedAt < remote.updatedAt
|
||
```
|
||
|
||
Следовательно, server должен сохранять `updatedAt` достаточно точно и не подменять его произвольным временем каждого чтения.
|
||
|
||
При конкурентных writes сервер должен иметь однозначное правило победителя.
|
||
|
||
## Deletions
|
||
|
||
Удаление создаёт tombstone/change:
|
||
|
||
```json
|
||
{
|
||
"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` — идентификатор устройства, а не механизм авторизации.
|