- 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>
6.0 KiB
Синхронизация
Цель
Один аккаунт 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 — идентификатор устройства, а не механизм авторизации.