Синхронизация
PowerSync - это слой синхронизации между клиентом (локальная SQLite через wa-sqlite / IndexedDB) и сервером (Supabase Postgres). Локальная SQLite является единственным источником истины для UI - хранилища читают только из SQLite, никогда напрямую из сети.
Как это работает
Supabase Postgres (сервер)
↕ логическая репликация (powersync_role, публикация powersync)
Сервис PowerSync (self-hosted, :8080)
↕ WebSocket sync stream (Sync Streams издание 3, per-user + глобальный)
Локальная SQLite (клиент, IDBBatchAtomicVFS / IndexedDB)
↕ подписки watchTable
Pinia-хранилища (UI)
При подключении PowerSync воспроизводит серверное состояние в локальную SQLite. После этого каждое серверное изменение передаётся по открытому WebSocket. Локальные записи идут в SQLite немедленно (оптимистично) и ставятся в очередь на загрузку.
Потоки синхронизации
Правила синхронизации определены в app/powersync/config/sync-config.yaml с использованием Sync Streams издания 3.
Поток на пользователя (user_data, auto_subscribe):
SELECT * FROM categories WHERE "userId" = auth.user_id()
SELECT * FROM wallets WHERE "userId" = auth.user_id()
SELECT * FROM trns WHERE "userId" = auth.user_id()
SELECT * FROM user_settings WHERE "userId" = auth.user_id()
Глобальный поток (rates): все строки доступны каждому авторизованному пользователю; строки пишет серверная edge-функция fetch-rates, не клиент.
SELECT * FROM rates
auth.user_id() разрешается в UUID пользователя Supabase из проверенного JWT. Сервис PowerSync проверяет JWT Supabase (ES256) через JWKS-endpoint, поэтому дополнительная логика токенов на клиенте не требуется.
watchTable - паттерн единственной подписки
watchTable(sql, params, onRows, throttleMs?) в app/services/powersync/db.ts:
- Выполняет SQL-запрос немедленно и вызывает
onRowsс текущими локальными строками. - Подписывается на события изменения таблицы. При изменении любой строки в таблице (локальная запись или входящая синхронизация) вызывает
onRowsсо свежими строками. throttleMsобъединяет быстрые всплески изменений (trailing edge). По умолчанию: 30мс. Транзакции используют 120мс для обработки большого потока строк при начальной синхронизации без лишних перерисовок UI.- Возвращает
AbortController- вызов.abort()отменяет подписку (хранилища делают это при разрушении).
Этот единственный механизм заменяет разделение на «инициализацию + подписку на реалтайм». Отдельного шага «загрузить из кеша, затем подписаться» не существует.
Подавление эха (транзакции)
Когда хранилище делает оптимистичную запись, а затем watchTable срабатывает для этой же записи, UI мог бы мерцать. useTrnsStore избегает этого с помощью reconcileTrns(prev, rows) (app/app/components/trns/reconcile.ts):
- Сравнивает входящие строки с предыдущим состоянием по
updatedAt. - Если ничего не изменилось - возвращает тот же
prev-ref (Vue не видит изменений, перерисовки нет). - Для изменённых строк запускает
rowToTrn()только для дельты - неизменённые объекты строк переиспользуются.
Очередь загрузки
Локальные записи автоматически ставятся в очередь PowerSync. SupabaseConnector.uploadData() (app/services/powersync/connector.ts) опустошает очередь:
| Операция PowerSync | Вызов Supabase |
|---|---|
PUT | supabase.from(table).upsert(row) |
PATCH | supabase.from(table).update(data).eq('id', id) |
DELETE | supabase.from(table).delete().eq('id', id) |
Коннектор работает от имени авторизованного пользователя Supabase, поэтому RLS-политики применяются при каждой загрузке. Это слой безопасности пути записи - путь чтения использует powersync_role (BYPASSRLS) для репликации, разграничение по пользователям обеспечивается правилами синхронизации.
Фатальные и повторяемые ошибки загрузки
| Класс ошибки | Коды Postgres | Поведение |
|---|---|---|
| Фатальные (данные / ограничение / RLS) | 22xxx, 23xxx, 42501 | Транзакция отбрасывается - очередь разблокируется; отброшенные операции авто-согласуются (см. ниже) |
| Повторяемые (сеть, таймаут) | всё остальное | Ошибка пробрасывается - PowerSync повторяет попытку с backoff |
Отброшенные операции классифицируются planDivergence() (app/services/powersync/uploadReconcile.ts) и согласуются автоматически:
- Отклонённые INSERT - локальные строки удаляются (id генерируются на клиенте, сервер их так и не принял), с каскадом на локальные транзакции, ссылающиеся на отменённый кошелёк/категорию. Toast:
sync.errors.uploadReverted. - Отклонённые UPDATE/DELETE - на сервере осталась предыдущая версия, которую PowerSync не может повторно вытянуть для отдельной строки, поэтому пользователю предлагается полная повторная синхронизация (очистка + повторная загрузка). Toast:
sync.errors.uploadDiverged.
Обработка удалений
PowerSync синхронизирует жёсткие удаления нативно - когда строка удаляется на сервере, сервис PowerSync передаёт событие удаления клиенту и SQLite удаляет строку. Никакого сравнения хешей или полного обновления не требуется.
Каскадные удаления (на клиенте): deleteWallet и deleteCategory также удаляют транзакции сущности из локальной SQLite до возврата. Без этого подписка watchTable на trns вернула бы осиротевшие строки в UI после удаления родительской сущности.
Первая синхронизация
waitForFirstSync(timeoutMs=30000) (app/services/powersync/db.ts) разрешается в true после полной передачи начального серверного состояния в локальную SQLite и в false по таймауту. Загрузка не блокируется на нём - кешированные/локальные данные отрисовываются сразу, а первая синхронизация завершается в фоне (awaitInitialSync в useInitApp.ts). Его результат лишь определяет показ экранов онбординга/ошибки, поэтому при свежем входе онбординг никогда не мелькает, пока данные ещё приходят.
Ключевые файлы
| Файл | Роль |
|---|---|
app/services/powersync/db.ts | Синглтон PowerSyncDatabase, connectPowerSync, watchTable, waitForFirstSync |
app/services/powersync/connector.ts | SupabaseConnector - fetchCredentials + uploadData |
app/services/powersync/AppSchema.ts | SQLite-схема клиента |
app/services/powersync/transforms.ts | rowToTrn, rowToWallet, rowToCategory, rowToRates, trnToRow и др. |
app/services/powersync/mutations.ts | upsertRow, deleteRow, upsertRows |
app/app/components/trns/useTrnsStore.ts | Подписка watchTable + подавление эха reconcileTrns |
app/app/components/trns/reconcile.ts | reconcileTrns - дедупликация и переиспользование неизменённых объектов строк |
app/app/plugins/powersync.client.ts | Подключает PowerSync при входе; ставит на паузу (сохраняя локальные данные и очередь) при непроизвольной потере сессии; явный выход стирает отдельно |
app/powersync/config/sync-config.yaml | Правила Sync Streams (per-user + глобальные rates) |
app/powersync/config/service.yaml | Конфиг сервиса PowerSync (репликация, JWT, порт 8080) |
app/supabase/powersync_setup.sql | powersync_role + публикация powersync |
Следующие шаги
- Офлайн-first - офлайн-записи, очередь загрузки и обработка фатальных ошибок
- Архитектура - инициализация приложения и паттерн хранилищ