Open Finapp
Справочник

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

Как PowerSync синхронизирует локальную SQLite с Supabase Postgres.

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
PUTsupabase.from(table).upsert(row)
PATCHsupabase.from(table).update(data).eq('id', id)
DELETEsupabase.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.tsSupabaseConnector - fetchCredentials + uploadData
app/services/powersync/AppSchema.tsSQLite-схема клиента
app/services/powersync/transforms.tsrowToTrn, rowToWallet, rowToCategory, rowToRates, trnToRow и др.
app/services/powersync/mutations.tsupsertRow, deleteRow, upsertRows
app/app/components/trns/useTrnsStore.tsПодписка watchTable + подавление эха reconcileTrns
app/app/components/trns/reconcile.tsreconcileTrns - дедупликация и переиспользование неизменённых объектов строк
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.sqlpowersync_role + публикация powersync

Следующие шаги

  • Офлайн-first - офлайн-записи, очередь загрузки и обработка фатальных ошибок
  • Архитектура - инициализация приложения и паттерн хранилищ