added AGENTS.md and docs in root dir
Android CI / Build and Test (push) Failing after 13m25s
iOS CI / Build and Test SwiftUI App (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build Unsigned iOS IPA (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build macOS ZIP (push) Canceled after 0s
Android CI / Build and Test (push) Failing after 13m25s
iOS CI / Build and Test SwiftUI App (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build Unsigned iOS IPA (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build macOS ZIP (push) Canceled after 0s
This commit is contained in:
1 parent
2f891c9503
commit
da9e17f45f
7 files changed
+728
-7
No files matched your search
@@ -0,0 +1,85 @@
|
||||
# Архитектура Balance (обзор всей системы)
|
||||
|
||||
Этот документ описывает, как связаны между собой три подпроекта.
|
||||
Детальная архитектура каждой платформы — в `docs/ARCHITECTURE.md`
|
||||
соответствующего подпроекта.
|
||||
|
||||
## Общая схема
|
||||
|
||||
```text
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ iOS клиент │ │ macOS клиент│ │watchOS клиент│ Apple: SwiftUI + SwiftData
|
||||
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ (Balance/)
|
||||
│ │ │
|
||||
└────────┬────────┴────────┬────────┘
|
||||
│ │
|
||||
HTTPS (или HTTP только для localhost)
|
||||
│ │
|
||||
┌────────┴────────┐ │
|
||||
│ Android клиент │ │ Kotlin + Jetpack Compose + Room
|
||||
│ (BalanceAndroid)│ │ (BalanceAndroid/)
|
||||
└────────┬────────┘ │
|
||||
│ │
|
||||
▼ ▼
|
||||
┌───────────────────────────────┐
|
||||
│ BalanceServer │ Go 1.25 + SQLite
|
||||
│ ┌───────────┬───────────────┐ │ (BalanceServer/)
|
||||
│ │ HTTP API │ Web-клиент │ │
|
||||
│ │ /v1/* │ (embedded) │ │
|
||||
│ └───────────┴───────────────┘ │
|
||||
└───────────────────────────────┘
|
||||
```
|
||||
|
||||
Каждый нативный клиент (iOS/macOS/watchOS/Android) хранит полную копию
|
||||
данных локально (SwiftData или Room) и работает офлайн. Сервер —
|
||||
единственная точка правды при конфликте между устройствами одного
|
||||
аккаунта: он хранит append-only лог изменений с курсором и применяет
|
||||
last-write-wins по времени изменения записи (`updatedAt`/`syncUpdatedAt`).
|
||||
|
||||
Web-клиент не является отдельным подпроектом — это статические файлы,
|
||||
встроенные (embed) в Go-бинарник сервера, использующие тот же HTTP API,
|
||||
что и нативные клиенты, но с cookie-based сессией вместо Bearer-токена.
|
||||
|
||||
## Компоненты сервера
|
||||
|
||||
- **HTTP API** (`internal/httpapi/`) — auth-эндпоинты и `/v1/sync`.
|
||||
- **auth** (`internal/auth/`) — пароли (Argon2id), JWT access/refresh
|
||||
токены.
|
||||
- **database** (`internal/database/`) — SQLite-хранилище, схема,
|
||||
индексы.
|
||||
- **config** (`internal/config/`) — переменные окружения.
|
||||
- **web** (`internal/httpapi/web.go`) — раздача встроенного
|
||||
web-приложения.
|
||||
|
||||
Подробнее — [`BalanceServer/docs/ARCHITECTURE.md`](../BalanceServer/docs/ARCHITECTURE.md).
|
||||
|
||||
## Компоненты Apple-клиента
|
||||
|
||||
- **Models** — SwiftData `@Model` сущности.
|
||||
- **Utilities/FinanceCalculations** — единый источник правил расчёта
|
||||
баланса и аналитики.
|
||||
- **Shared/ServerSync** — HTTP-клиент, авторизация, sync orchestration,
|
||||
Keychain.
|
||||
- **Views** — общий SwiftUI UI; `BalanceMac/` и `BalanceWatch/` —
|
||||
платформенно-специфичные точки входа и экраны.
|
||||
|
||||
Подробнее — [`Balance/docs/ARCHITECTURE.md`](../Balance/docs/ARCHITECTURE.md).
|
||||
|
||||
## Компоненты Android-клиента
|
||||
|
||||
- **data/** — Room entities, DAO, `FinanceMath` (та же роль, что и
|
||||
`FinanceCalculations` в Apple-клиенте).
|
||||
- **sync/** — `ServerApi` (HTTP/auth), `ServerPreferences` (URL/cursor),
|
||||
`SecureSessionStore` (Android Keystore), `SyncRepository`
|
||||
(orchestration), `SyncWorker` (периодический фон).
|
||||
- **ui/** — Jetpack Compose экраны, состояние из `MainViewModel`.
|
||||
|
||||
Подробнее — [`BalanceAndroid/docs/ARCHITECTURE.md`](../BalanceAndroid/docs/ARCHITECTURE.md).
|
||||
|
||||
## Принцип согласованности между платформами
|
||||
|
||||
Три клиента реализуют одну и ту же доменную модель и один и тот же
|
||||
sync-протокол независимо друг от друга, на разных стеках. Это
|
||||
осознанный компромисс (нативный UX на каждой платформе) с обратной
|
||||
стороной: любое изменение общего контракта нужно вносить согласованно
|
||||
в трёх местах. См. `docs/SYNC_PROTOCOL.md` и `docs/DATA_MODEL.md`.
|
||||
@@ -0,0 +1,108 @@
|
||||
# Сквозная модель данных
|
||||
|
||||
Три платформы реализуют одну и ту же доменную модель независимо, на
|
||||
своём стеке хранения. Этот документ сопоставляет поля между ними.
|
||||
Полные описания — [`Balance/docs/DATA_MODEL.md`](../Balance/docs/DATA_MODEL.md),
|
||||
[`BalanceAndroid/docs/DATA_MODEL.md`](../BalanceAndroid/docs/DATA_MODEL.md),
|
||||
[`BalanceServer/docs/DATA_MODEL.md`](../BalanceServer/docs/DATA_MODEL.md).
|
||||
|
||||
## Транзакция
|
||||
|
||||
| Логическое поле | Apple (`FinanceTransaction`) | Android (`TransactionEntity`) | Сервер |
|
||||
| --- | --- | --- | --- |
|
||||
| Идентификатор | `id: UUID` | `id: String` (UUID) | stable ID |
|
||||
| Сумма | `amount: Double` | `amount: Double` | amount |
|
||||
| Дата операции | `date: Date` | `date: Long` (epoch millis) | date |
|
||||
| Заметка | `note: String` | `note: String` | note |
|
||||
| Имя категории (snapshot) | `categoryName: String` | `categoryName: String` | category presentation data |
|
||||
| Иконка категории | `categoryIcon: String` (SF Symbol) | `categoryIcon: String` | — |
|
||||
| Emoji категории | `categoryEmoji: String` | `categoryEmoji: String` | — |
|
||||
| Цвет категории | `categoryColorName: String` | `categoryColorName: String` | — |
|
||||
| Корректировка баланса | `isBalanceAdjustment: Bool` | `isBalanceAdjustment: Boolean` | balance-adjustment flag |
|
||||
| Тип операции | `kindRawValue: String` (`income`/`expense`) | `kindRawValue: String` | transaction kind |
|
||||
| Версия/время изменения | `syncUpdatedAt: Date` | `updatedAt: Long` | `updatedAt` |
|
||||
| Владелец | сессия аккаунта | сессия аккаунта | принадлежность пользователю |
|
||||
|
||||
**Категория хранится snapshot'ом**, а не ссылкой, на всех платформах:
|
||||
это позволяет не терять отображение старой операции при удалении
|
||||
категории и синхронизировать запись без relationship. Из-за этого
|
||||
переименование пользовательской категории должно явно обновлять уже
|
||||
существующие транзакции и бюджеты — и Apple-, и Android-клиент это
|
||||
делают.
|
||||
|
||||
## Пользовательская категория
|
||||
|
||||
| Логическое поле | Apple (`CustomCategory`) | Android (`categories`) | Сервер |
|
||||
| --- | --- | --- | --- |
|
||||
| Идентификатор | `id: UUID` | `id` | stable ID |
|
||||
| Имя | `name: String` | `name` | name |
|
||||
| Иконка | `icon: String` | `icon` | icon/emoji |
|
||||
| Emoji | `emoji: String` | `emoji` | — |
|
||||
| Цвет | `colorName: String` | `colorName` | color |
|
||||
| Тип | `kindRawValue: String` | `kindRawValue` | operation kind |
|
||||
| Создание | `createdAt: Date` | `createdAt` | creation metadata |
|
||||
| Версия | `syncUpdatedAt: Date` | `updatedAt` | update metadata |
|
||||
|
||||
Имя пользовательской категории уникально без учёта регистра/диакритики
|
||||
относительно системных и других пользовательских категорий (проверяется
|
||||
на клиентах). Встроенные системные категории (Продукты, Транспорт,
|
||||
Зарплата и т.д.) не персистятся как отдельные записи ни на одной
|
||||
платформе — они закодированы в клиентском коде.
|
||||
|
||||
## Месячный бюджет
|
||||
|
||||
| Логическое поле | Apple (`MonthlyBudget`) | Android (`budgets`) | Сервер |
|
||||
| --- | --- | --- | --- |
|
||||
| Идентификатор | `id: UUID` | `id` | stable ID |
|
||||
| Имя категории | `categoryName: String` | `categoryName` | category information |
|
||||
| Иконка/emoji категории | `categoryIcon`, `categoryEmoji` | `categoryIcon`, `categoryEmoji` | — |
|
||||
| Лимит | `limit: Double` | `limitAmount: Double` | monthly limit |
|
||||
| Начало месяца | `monthStart: Date` | `monthStart: Long` | month start |
|
||||
| Версия | `syncUpdatedAt: Date` | `updatedAt: Long` | `updatedAt` |
|
||||
|
||||
Бюджет идентифицируется собственным UUID, но связывается с категорией
|
||||
по сохранённому имени (`categoryName`), а не по ссылке.
|
||||
|
||||
## Tombstone / deletion journal
|
||||
|
||||
| Логическое поле | Apple (`SyncTombstone`) | Android (`deletion_journal`) | Сервер (change record) |
|
||||
| --- | --- | --- | --- |
|
||||
| Идентификатор записи | `id: UUID` | `id = entity:recordId` | record ID |
|
||||
| Тип сущности | `entity: String` | `entity` | entity |
|
||||
| ID удалённой записи | `recordID: String` | `recordId` | id |
|
||||
| Время | `updatedAt: Date` | `updatedAt: Long` | `updatedAt` |
|
||||
| Флаг удаления | подразумевается (`deleted: true` в payload) | подразумевается | `deleted: true` |
|
||||
|
||||
Поддерживаемые значения `entity` одинаковы на всех платформах:
|
||||
`transaction`, `category`, `budget`.
|
||||
|
||||
## Финансовые инварианты (одинаковы на Apple и Android)
|
||||
|
||||
1. `amount` не может быть отрицательным через обычный UI.
|
||||
2. Общий баланс = `sum(income.amount) − sum(expense.amount)`; записи с
|
||||
`isBalanceAdjustment == true` **учитываются** в этой сумме.
|
||||
3. Периодическая аналитика **исключает** balance adjustments и записи
|
||||
вне выбранного периода.
|
||||
4. Расходы группируются по `categoryName` (не по ID категории).
|
||||
5. `syncUpdatedAt`/`updatedAt` обязано меняться при каждом изменении
|
||||
синхронизируемой сущности.
|
||||
6. Удаление синхронизируемой сущности обязано создать tombstone —
|
||||
прямой `DELETE`/`delete()` без tombstone запрещён.
|
||||
|
||||
Источники правды по расчётам:
|
||||
`Balance/Balance/Utilities/FinanceCalculations.swift` (Apple) и
|
||||
`BalanceAndroid/.../data/FinanceMath.kt` (Android). При изменении
|
||||
правил на одной платформе перенеси изменение на другую.
|
||||
|
||||
## Инварианты на стороне сервера
|
||||
|
||||
1. Record ID стабилен между синхронизациями.
|
||||
2. Запись принадлежит ровно одному пользователю; клиент не может
|
||||
читать/изменять чужие записи.
|
||||
3. `sequence` (серверный порядок изменений) не должен уменьшаться.
|
||||
4. Повторная доставка одного и того же изменения не должна повреждать
|
||||
данные (идемпотентность).
|
||||
5. Удаление обязано быть видимым клиенту через следующий pull.
|
||||
6. Все timestamps хранятся в UTC.
|
||||
7. Enum/строковые значения (`kindRawValue` и т.п.) совместимы между
|
||||
iOS и Android — не переименовывать без периода совместимости.
|
||||
@@ -0,0 +1,106 @@
|
||||
# Разработка (все платформы)
|
||||
|
||||
Сводная шпаргалка по сборке, запуску и проверке всех трёх подпроектов.
|
||||
Подробности — в `docs/DEVELOPMENT.md` каждого подпроекта.
|
||||
|
||||
## Требования по платформам
|
||||
|
||||
| Подпроект | Инструменты | Минимальные версии |
|
||||
| --- | --- | --- |
|
||||
| `Balance/` | Xcode, Swift | Xcode 16+, iOS 17+, macOS 14+, watchOS 10+ |
|
||||
| `BalanceAndroid/` | Android Studio / Gradle | AGP 8.13, Kotlin 2.3.21, compileSdk 36, minSdk 26, Java 17 |
|
||||
| `BalanceServer/` | Go | Go 1.25.0+ |
|
||||
|
||||
## Локальный запуск всей системы
|
||||
|
||||
Порядок, удобный для end-to-end проверки sync между платформами:
|
||||
|
||||
```bash
|
||||
# 1. Поднять сервер
|
||||
cd BalanceServer
|
||||
cp .env.example .env
|
||||
# задать BALANCE_JWT_SECRET (см. README.md)
|
||||
go mod tidy
|
||||
go run ./cmd/balance-server
|
||||
# сервер слушает http://localhost:8080
|
||||
|
||||
# 2. Открыть Apple-проект и указать http://localhost:8080 в Settings → Server account
|
||||
cd ../Balance
|
||||
open Balance.xcodeproj
|
||||
|
||||
# 3. Собрать и запустить Android-приложение
|
||||
cd ../BalanceAndroid
|
||||
./gradlew assembleDebug
|
||||
# на эмуляторе сервер локального хоста доступен как http://10.0.2.2:8080
|
||||
```
|
||||
|
||||
Для физического iPhone/Android-устройства `localhost` указывает на само
|
||||
устройство, а не на машину разработчика — используйте сетевой адрес
|
||||
или HTTPS reverse proxy (см. `docs/SECURITY.md`).
|
||||
|
||||
## Сборка и тесты по отдельности
|
||||
|
||||
### Apple (`Balance/`)
|
||||
|
||||
```bash
|
||||
xcodebuild -project Balance.xcodeproj -list
|
||||
|
||||
xcodebuild -project Balance.xcodeproj -scheme Balance \
|
||||
-destination 'generic/platform=iOS' build
|
||||
|
||||
xcodebuild -project Balance.xcodeproj -scheme Balance \
|
||||
-destination 'platform=iOS Simulator,name=<available iPhone>' test
|
||||
```
|
||||
|
||||
Схемы: `Balance` (iOS), `BalanceMac`, `BalanceWatch`. Без
|
||||
`BalanceCloudKitEnabled=YES` приложение работает в local-mode
|
||||
(SwiftData без CloudKit) — рекомендуемый режим для разработки.
|
||||
|
||||
### Android (`BalanceAndroid/`)
|
||||
|
||||
```bash
|
||||
./gradlew clean test assembleDebug
|
||||
```
|
||||
|
||||
Тесты — `app/src/test/FinanceMathTest`. `local.properties` с `sdk.dir`
|
||||
не должен попадать в Git.
|
||||
|
||||
### Server (`BalanceServer/`)
|
||||
|
||||
```bash
|
||||
go mod download
|
||||
go test ./...
|
||||
go test -race ./...
|
||||
go vet ./...
|
||||
go build ./...
|
||||
```
|
||||
|
||||
Docker-путь — `docker compose up -d --build` (см.
|
||||
[`BalanceServer/README.md`](../BalanceServer/README.md)).
|
||||
|
||||
## Что проверять при изменении общего контракта
|
||||
|
||||
Если PR меняет sync API, модель данных или финансовые расчёты — прогони
|
||||
проверки во всех трёх подпроектах, а не только в изменённом:
|
||||
|
||||
```text
|
||||
old iOS client + new server
|
||||
new iOS client + server
|
||||
Android client + server
|
||||
concurrent sync с двух устройств одного аккаунта
|
||||
login/refresh после истечения access token
|
||||
delete → pull на другом устройстве
|
||||
```
|
||||
|
||||
## Типовые проблемы синхронизации (одинаковый чек-лист для обеих платформ)
|
||||
|
||||
1. Проверь server URL в настройках клиента.
|
||||
2. Проверь правило HTTPS/localhost (см. `docs/SECURITY.md`).
|
||||
3. Проверь авторизацию/сессию (login, не истёк ли refresh).
|
||||
4. Проверь дату последней синхронизации и сохранённый cursor.
|
||||
5. Проверь совместимость версии клиента и `BalanceServer`.
|
||||
6. Если данные не удаляются на другом устройстве — убедись, что
|
||||
удаление прошло через tombstone-путь
|
||||
(`modelContext.deleteForSync` на Apple,
|
||||
`FinanceDao.deleteTransaction/deleteCategory/deleteBudget` на
|
||||
Android), а не через прямой `delete()`.
|
||||
@@ -0,0 +1,95 @@
|
||||
# Безопасность (сквозные требования)
|
||||
|
||||
Этот документ собирает требования безопасности, которые применяются
|
||||
ко **всем** подпроектам одновременно. Платформенно-специфичные детали —
|
||||
в [`BalanceServer/docs/SECURITY.md`](../BalanceServer/docs/SECURITY.md),
|
||||
`Balance/AGENTS.md` (раздел «Безопасность») и
|
||||
`BalanceAndroid/AGENTS.md` (раздел «Security»).
|
||||
|
||||
## Хранение секретов на клиентах
|
||||
|
||||
| | Apple | Android |
|
||||
| --- | --- | --- |
|
||||
| Access/refresh токены | Keychain (service `BalanceServer`) | зашифрованное хранилище через Android Keystore (AES/GCM), `SecureSessionStore` |
|
||||
| Несекретные настройки (URL сервера, cursor, sync metadata) | `UserDefaults` | `ServerPreferences` |
|
||||
| Запрещено | пароль в `UserDefaults`, SwiftData или файлах | токены в обычных `SharedPreferences` |
|
||||
|
||||
Ни один клиент не должен хранить пароль пользователя после
|
||||
аутентификации — только access/refresh токены.
|
||||
|
||||
## Хранение секретов на сервере
|
||||
|
||||
- Пароли — только Argon2id-хеши, plaintext никогда не сохраняется.
|
||||
- Refresh-токены хранятся как SHA-256 хеши, не в открытом виде.
|
||||
- `BALANCE_JWT_SECRET` должен быть случайной строкой не короче 32
|
||||
символов; его смена инвалидирует access-токены (refresh-сессии
|
||||
остаются валидны и получают новый access-токен при следующем
|
||||
refresh).
|
||||
|
||||
## Транспорт
|
||||
|
||||
- Для удалённого (не-localhost) сервера все клиенты обязаны
|
||||
использовать HTTPS.
|
||||
- HTTP разрешён клиентам **только** для `localhost`, `127.0.0.1` и
|
||||
`::1` — это относится и к Apple-, и к Android-клиенту.
|
||||
- Production-деплой сервера должен работать через HTTPS/TLS
|
||||
(например, через reverse proxy — Caddy/nginx), см.
|
||||
[`BalanceServer/README.md#https`](../BalanceServer/README.md#https).
|
||||
|
||||
## Логирование
|
||||
|
||||
Запрещено логировать на любой из трёх платформ:
|
||||
|
||||
- пароли;
|
||||
- access/refresh токены;
|
||||
- заголовок `Authorization` целиком;
|
||||
- полные request bodies с финансовыми данными без явной необходимости.
|
||||
|
||||
Безопасно логировать (на сервере): request ID, endpoint, status code,
|
||||
latency, размер batch, неконфиденциальные идентификаторы записей.
|
||||
|
||||
## Авторизация на сервере
|
||||
|
||||
Каждая sync-мутация обязана проверять ownership по authenticated user
|
||||
из токена/сессии. Никогда не доверяй `userId`, пришедшему в теле
|
||||
запроса от клиента, если identity уже известна из
|
||||
Authorization-заголовка или cookie-сессии:
|
||||
|
||||
```text
|
||||
Authorization header / session cookie
|
||||
↓
|
||||
authenticated user
|
||||
↓
|
||||
server-side ownership check
|
||||
↓
|
||||
requested entity
|
||||
```
|
||||
|
||||
## Валидация входных данных (сервер)
|
||||
|
||||
Проверяются: UUID/ID, enum-значения, `amount`, timestamps, длины строк,
|
||||
размер batch, размер JSON, границы cursor/sequence.
|
||||
|
||||
## Защита от злоупотреблений (production-деплой сервера)
|
||||
|
||||
- rate limiting для auth-эндпоинтов (встроенный лимитер — 30
|
||||
auth-запросов на IP в минуту; для более сильного публичного
|
||||
rate limiting используйте reverse proxy);
|
||||
- разумные ограничения на размер тела запроса;
|
||||
- таймауты и graceful shutdown;
|
||||
- ограничение подключений к БД;
|
||||
- мониторинг.
|
||||
|
||||
## Секреты в репозитории
|
||||
|
||||
Ни в одном из трёх подпроектов не должны появляться:
|
||||
|
||||
- production URL серверов;
|
||||
- реальные `BALANCE_JWT_SECRET` или иные секреты;
|
||||
- закоммиченные `.env`, keystore-файлы, сертификаты;
|
||||
- захардкоженные тестовые credentials, которые могут быть спутаны с
|
||||
боевыми.
|
||||
|
||||
`.env.example` в `BalanceServer/` и `local.properties.example` в
|
||||
`BalanceAndroid/` — единственные допустимые «примерные» файлы с
|
||||
конфигурацией; реальные значения в них не хранятся.
|
||||
@@ -0,0 +1,88 @@
|
||||
# Единый протокол синхронизации
|
||||
|
||||
Все клиенты (iOS, macOS, watchOS, Android, web) синхронизируются с
|
||||
`BalanceServer` через один и тот же HTTP(S) API и один и тот же набор
|
||||
инвариантов. Это сводка контракта; детальные описания на стороне
|
||||
сервера — [`BalanceServer/docs/API.md`](../BalanceServer/docs/API.md) и
|
||||
[`BalanceServer/docs/SYNC.md`](../BalanceServer/docs/SYNC.md).
|
||||
Клиентские реализации — [`Balance/docs/SYNC.md`](../Balance/docs/SYNC.md)
|
||||
и [`BalanceAndroid/docs/SYNC.md`](../BalanceAndroid/docs/SYNC.md).
|
||||
|
||||
## Эндпоинты
|
||||
|
||||
| Метод | Путь | Назначение |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/health` | Health check |
|
||||
| `POST` | `/v1/auth/register` | Создание аккаунта |
|
||||
| `POST` | `/v1/auth/login` | Вход, выдача access/refresh токенов |
|
||||
| `POST` | `/v1/auth/refresh` | Ротация токенов |
|
||||
| `POST` | `/v1/auth/logout` | Отзыв refresh-токена |
|
||||
| `GET` | `/v1/me` | Текущий аккаунт |
|
||||
| `POST` | `/v1/sync` | Push и pull изменений |
|
||||
|
||||
Нативные клиенты используют `Authorization: Bearer <access-token>`.
|
||||
Web-клиент использует `HttpOnly`/`SameSite=Strict` cookie-сессию —
|
||||
это единственное отличие транспорта, сама sync-семантика идентична.
|
||||
|
||||
## Основные инварианты
|
||||
|
||||
1. **Cursor** — каждый клиент хранит курсор последнего успешно
|
||||
применённого изменения. Курсор сохраняется только после успешного
|
||||
применения соответствующего batch, не раньше.
|
||||
2. **Sequence** — сервер поддерживает глобальный монотонный порядок
|
||||
изменений в change stream; порядок не должен уменьшаться и должен
|
||||
быть однозначным при конкурентных запросах.
|
||||
3. **Push** — клиент отправляет свои изменения (`POST /v1/sync`);
|
||||
сервер валидирует, проверяет владение записью (ownership) и
|
||||
добавляет изменение в change stream. Повторный push (retry) не
|
||||
должен создавать вторую логическую запись из одного и того же
|
||||
client-change (идемпотентность).
|
||||
4. **Pull** — клиент запрашивает изменения после своего курсора;
|
||||
сервер никогда не возвращает чужие данные (изоляция по аккаунту) и
|
||||
поддерживает постраничный pull с продолжением от нового курсора.
|
||||
5. **Conflict resolution** — last-write-wins по `updatedAt`
|
||||
(`syncUpdatedAt` на Apple, `updatedAt` на Android/сервере). Сервер —
|
||||
единственный источник правды в этом сравнении, поэтому не должен
|
||||
произвольно перезаписывать `updatedAt` при каждом чтении.
|
||||
6. **Удаления (tombstones)** — удаление сущности не является простым
|
||||
`DELETE`; оно создаёт запись изменения вида
|
||||
`{ "entity": ..., "id": ..., "deleted": true, "updatedAt": ... }`,
|
||||
которая проходит через тот же change stream, что и обычные
|
||||
изменения. На Apple это `SyncTombstone`/`modelContext.deleteForSync`,
|
||||
на Android — `deletion_journal` через DAO-методы `deleteTransaction` /
|
||||
`deleteCategory` / `deleteBudget`.
|
||||
7. **Batch** — Apple-клиент отправляет батчи до 100 изменений и может
|
||||
делить крупные payload'ы; сервер не должен предполагать, что все
|
||||
изменения аккаунта приходят одним запросом. Android должен
|
||||
оставаться совместимым с этим же батчингом.
|
||||
8. **Reset/full sync** — если клиент теряет курсор, сервер должен
|
||||
поддерживать предусмотренный API механизм полного pull или новый
|
||||
baseline-курсор. Неизвестный/просроченный курсор нельзя молча
|
||||
трактовать как актуальный.
|
||||
|
||||
## Синхронизируемые сущности
|
||||
|
||||
- **Транзакция** (доход/расход, сумма, дата, заметка, snapshot
|
||||
категории);
|
||||
- **Пользовательская категория** (имя, иконка/emoji, цвет);
|
||||
- **Месячный бюджет** (категория, месяц, лимит);
|
||||
- **Tombstone / deletion journal** запись для удалений.
|
||||
|
||||
Подробное пополевое сопоставление — [`docs/DATA_MODEL.md`](DATA_MODEL.md).
|
||||
|
||||
## Что нельзя менять без координации
|
||||
|
||||
Любое из следующих изменений — breaking change для всех клиентов
|
||||
одновременно, даже если каждый подпроект по отдельности компилируется
|
||||
и проходит собственные тесты:
|
||||
|
||||
- имена/типы JSON-полей в payload'ах `/v1/sync` и auth-эндпоинтов;
|
||||
- HTTP-методы или пути эндпоинтов;
|
||||
- семантика курсора/sequence;
|
||||
- правило разрешения конфликтов (last-write-wins по `updatedAt`);
|
||||
- формат tombstone-записи.
|
||||
|
||||
При таком изменении обнови одновременно: `BalanceServer` (API +
|
||||
хендлеры + `docs/API.md`/`docs/SYNC.md`), `Balance/Shared/ServerSync.swift`
|
||||
(+ `Balance/docs/SYNC.md`), `BalanceAndroid/.../sync/*`
|
||||
(+ `BalanceAndroid/docs/SYNC.md`) и этот файл.
|
||||
Reference in new issue
Block a user