[{"data":1,"prerenderedAt":640},["ShallowReactive",2],{"navigation_docs_ru":3,"-ru-reference-offline-first":167,"-ru-reference-offline-first-surround":635},[4,61,101,146],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":60},"Руководство","i-lucide-book-open","\u002Fru\u002Fguide","ru\u002F1.guide",[10,15,20,25,30,35,40,45,50,55],{"title":11,"path":12,"stem":13,"icon":14},"Введение","\u002Fru\u002Fguide\u002Fintroduction","ru\u002F1.guide\u002F01.introduction","i-lucide-house",{"title":16,"path":17,"stem":18,"icon":19},"Установка","\u002Fru\u002Fguide\u002Finstallation","ru\u002F1.guide\u002F02.installation","i-lucide-smartphone",{"title":21,"path":22,"stem":23,"icon":24},"Авторизация","\u002Fru\u002Fguide\u002Fauth","ru\u002F1.guide\u002F03.auth","i-lucide-lock",{"title":26,"path":27,"stem":28,"icon":29},"Кошельки","\u002Fru\u002Fguide\u002Fwallets","ru\u002F1.guide\u002F04.wallets","i-lucide-wallet",{"title":31,"path":32,"stem":33,"icon":34},"Категории","\u002Fru\u002Fguide\u002Fcategories","ru\u002F1.guide\u002F05.categories","i-lucide-tags",{"title":36,"path":37,"stem":38,"icon":39},"Транзакции","\u002Fru\u002Fguide\u002Ftransactions","ru\u002F1.guide\u002F06.transactions","i-lucide-receipt",{"title":41,"path":42,"stem":43,"icon":44},"Переводы","\u002Fru\u002Fguide\u002Ftransfers","ru\u002F1.guide\u002F07.transfers","i-lucide-arrow-left-right",{"title":46,"path":47,"stem":48,"icon":49},"Статистика","\u002Fru\u002Fguide\u002Fstatistics","ru\u002F1.guide\u002F08.statistics","i-lucide-bar-chart-3",{"title":51,"path":52,"stem":53,"icon":54},"Тема","\u002Fru\u002Fguide\u002Ftheme","ru\u002F1.guide\u002F09.theme","i-lucide-palette",{"title":56,"path":57,"stem":58,"icon":59},"Настройки","\u002Fru\u002Fguide\u002Fsettings","ru\u002F1.guide\u002F10.settings","i-lucide-settings",false,{"title":62,"icon":63,"path":64,"stem":65,"children":66,"page":60},"Разработка","i-lucide-code","\u002Fru\u002Fdevelopment","ru\u002F2.development",[67,71,76,81,86,91,96],{"title":16,"path":68,"stem":69,"icon":70},"\u002Fru\u002Fdevelopment\u002Finstallation","ru\u002F2.development\u002F01.installation","i-lucide-download",{"title":72,"path":73,"stem":74,"icon":75},"Граф кодовой базы","\u002Fru\u002Fdevelopment\u002Funderstand-anything","ru\u002F2.development\u002F02.understand-anything","i-lucide-network",{"title":77,"path":78,"stem":79,"icon":80},"Офлайн и PWA","\u002Fru\u002Fdevelopment\u002Foffline","ru\u002F2.development\u002F03.offline","i-lucide-wifi-off",{"title":82,"path":83,"stem":84,"icon":85},"История миграций данных","\u002Fru\u002Fdevelopment\u002Fmigration","ru\u002F2.development\u002F04.migration","i-lucide-database",{"title":87,"path":88,"stem":89,"icon":90},"Деплой","\u002Fru\u002Fdevelopment\u002Fdeployment","ru\u002F2.development\u002F05.deployment","i-lucide-rocket",{"title":92,"path":93,"stem":94,"icon":95},"Тестирование","\u002Fru\u002Fdevelopment\u002Ftesting","ru\u002F2.development\u002F06.testing","i-lucide-flask-conical",{"title":97,"path":98,"stem":99,"icon":100},"Дата-утилиты","\u002Fru\u002Fdevelopment\u002Fdate-utilities","ru\u002F2.development\u002F07.date-utilities","i-lucide-calendar",{"title":102,"icon":103,"path":104,"stem":105,"children":106,"page":60},"Справочник","i-lucide-file-code","\u002Fru\u002Freference","ru\u002F3.reference",[107,112,116,121,126,131,136,141],{"title":108,"path":109,"stem":110,"icon":111},"Архитектура","\u002Fru\u002Freference\u002Farchitecture","ru\u002F3.reference\u002F01.architecture","i-lucide-boxes",{"title":113,"path":114,"stem":115,"icon":44},"Типы транзакций","\u002Fru\u002Freference\u002Ftransaction-types","ru\u002F3.reference\u002F02.transaction-types",{"title":117,"path":118,"stem":119,"icon":120},"Синхронизация","\u002Fru\u002Freference\u002Fsync","ru\u002F3.reference\u002F03.sync","i-lucide-refresh-cw",{"title":122,"path":123,"stem":124,"icon":125},"Офлайн-first","\u002Fru\u002Freference\u002Foffline-first","ru\u002F3.reference\u002F04.offline-first","i-lucide-list-ordered",{"title":127,"path":128,"stem":129,"icon":130},"Тех. решения","\u002Fru\u002Freference\u002Ftech-decisions","ru\u002F3.reference\u002F05.tech-decisions","i-lucide-lightbulb",{"title":132,"path":133,"stem":134,"icon":135},"Валидация","\u002Fru\u002Freference\u002Fvalidation-strategy","ru\u002F3.reference\u002F06.validation-strategy","i-lucide-shield-check",{"title":137,"path":138,"stem":139,"icon":140},"Что изменилось со времён Firebase","\u002Fru\u002Freference\u002Ffirebase-migration","ru\u002F3.reference\u002F07.firebase-migration","i-lucide-hamburger",{"title":142,"path":143,"stem":144,"icon":145},"Производительность","\u002Fru\u002Freference\u002Fperformance","ru\u002F3.reference\u002F08.performance","i-lucide-gauge",{"title":147,"icon":148,"path":149,"stem":150,"children":151,"page":60},"Премиум","i-lucide-star","\u002Fru\u002Fpremium","ru\u002F4.premium",[152,157,162],{"title":153,"path":154,"stem":155,"icon":156},"Обзор","\u002Fru\u002Fpremium\u002Foverview","ru\u002F4.premium\u002F01.overview","i-lucide-layers",{"title":158,"path":159,"stem":160,"icon":161},"Telegram-бот","\u002Fru\u002Fpremium\u002Ftelegram-bot","ru\u002F4.premium\u002F02.telegram-bot","i-lucide-send",{"title":163,"path":164,"stem":165,"icon":166},"AI Chat","\u002Fru\u002Fpremium\u002Fai-chat","ru\u002F4.premium\u002F03.ai-chat","i-lucide-sparkles",{"id":168,"title":122,"body":169,"description":627,"extension":628,"links":629,"meta":630,"navigation":631,"path":123,"seo":632,"stem":124,"__hash__":634},"docs_ru\u002Fru\u002F3.reference\u002F04.offline-first.md",{"type":170,"value":171,"toc":611},"minimark",[172,176,181,192,195,199,202,218,221,225,241,324,328,334,339,365,389,393,400,404,422,425,429,440,444,447,454,458,594,598],[173,174,175],"p",{},"Финапка работает в режиме офлайн-first через PowerSync. Локальная SQLite является единственным источником истины. Все операции чтения берутся из SQLite; все записи сначала идут в SQLite и ставятся в очередь для загрузки в Supabase Postgres.",[177,178,180],"h2",{"id":179},"как-это-работает","Как это работает",[182,183,188],"pre",{"className":184,"code":186,"language":187},[185],"language-text","действие пользователя (создание\u002Fобновление\u002Fудаление)\n  → оптимистично: items.value обновляется в хранилище немедленно\n  → upsertRow \u002F deleteRow записывает в локальную SQLite (синхронно)\n    → успех: watchTable срабатывает → хранилище обновляется из SQLite\n    → ошибка: восстанавливается prev-снимок + showErrorToast (ошибка локальной записи)\n  → PowerSync ставит операцию в очередь на загрузку\n    → онлайн: коннектор опустошает очередь → upsert\u002Fupdate\u002Fdelete в Supabase\n    → офлайн: очередь сохраняется в SQLite; опустошается при переподключении\n","text",[189,190,186],"code",{"__ignoreMap":191},"",[173,193,194],{},"Ручного управления очередью нет, правил свёртки нет, системы frontend-local ID нет. Очередью загрузки управляет PowerSync.",[177,196,198],{"id":197},"офлайн-записи","Офлайн-записи",[173,200,201],{},"При отсутствии соединения:",[203,204,205,209,212,215],"ul",{},[206,207,208],"li",{},"Чтение продолжает работать - все данные уже в локальной SQLite.",[206,210,211],{},"Записи идут в локальную SQLite немедленно (оптимистично). Пользователь видит изменение сразу.",[206,213,214],{},"Операция загрузки добавляется во внутреннюю очередь PowerSync.",[206,216,217],{},"При переподключении PowerSync автоматически опустошает очередь.",[173,219,220],{},"Никаких действий пользователя для запуска синхронизации после переподключения не требуется.",[177,222,224],{"id":223},"потеря-сессии-в-офлайне","Потеря сессии в офлайне",[173,226,227,228,232,233,236,237,240],{},"Уход в офлайн не разлогинивает пользователя. Неудачное обновление токена в офлайне считается ",[229,230,231],"strong",{},"повторяемым"," - supabase-js сохраняет персистнутую сессию (localStorage) и не шлёт ",[189,234,235],{},"SIGNED_OUT",", поэтому роут-гард (",[189,238,239],{},"hasPersistedSession()",") оставляет пользователя в приложении, а несинхронизированная очередь продолжает наполняться. Два случая обрабатываются так, чтобы офлайн-записи не терялись:",[203,242,243,280],{},[206,244,245,248,249,252,253,256,257,260,261,264,265,268,269,272,273,275,276,279],{},[229,246,247],{},"Реактивная сессия ещё не зарезолвилась (холодный старт в офлайне)."," После протухания access-токена свежий ",[189,250,251],{},"getSession()"," возвращает ",[189,254,255],{},"null",", поэтому реактивный ",[189,258,259],{},"uid"," равен null до переподключения. Записи проставляют ",[189,262,263],{},"userId"," через ",[189,266,267],{},"resolveWriteUid(uid.value)"," (",[189,270,271],{},"app\u002Fapp\u002Fcomposables\u002FuseAuthSession.ts","), который откатывается к синхронно-персистнутому uid. Без этого строка получила бы пустой ",[189,274,263],{},", который RLS отклоняет (",[189,277,278],{},"42501",") при загрузке - молча теряя офлайн-запись.",[206,281,282,285,286,288,289,268,292,295,296,299,300,303,304,307,308,311,312,315,316,319,320,323],{},[229,283,284],{},"Сессия потеряна непроизвольно (токен отозван \u002F истёк на сервере)."," Всплывает при переподключении как настоящий ",[189,287,235],{},". Тогда watcher авторизации ",[229,290,291],{},"ставит синхронизацию на паузу",[189,293,294],{},"pausePowerSync"," - ",[189,297,298],{},"db.disconnect()",", без очистки), а не стирает, сохраняя локальную SQLite ",[229,301,302],{},"и несинхронизированную очередь",". При наличии ожидающих записей показывается toast на повторный вход (",[189,305,306],{},"sync.errors.sessionLostPending","); повторная авторизация тем же пользователем переподключает и опустошает очередь. Только ",[229,309,310],{},"явный"," выход (",[189,313,314],{},"useUserStore.signOut()",") стирает локальные данные через ",[189,317,318],{},"disconnectPowerSync"," → ",[189,321,322],{},"disconnectAndClear",".",[177,325,327],{"id":326},"обработка-ошибок-загрузки","Обработка ошибок загрузки",[173,329,330,333],{},[189,331,332],{},"SupabaseConnector.uploadData()"," разделяет фатальные ошибки и повторяемые:",[335,336,338],"h3",{"id":337},"фатальные-ошибки-отбрасываются","Фатальные ошибки (отбрасываются)",[173,340,341,342,345,346,349,350,352,353,356,357,360,361,364],{},"Классы ошибок Postgres ",[189,343,344],{},"22xxx"," (исключение данных), ",[189,347,348],{},"23xxx"," (нарушение ограничения целостности) и ",[189,351,278],{}," (RLS - недостаточно прав) считаются фатальными. Неудавшаяся операция ",[229,354,355],{},"и все операции в очереди после неё"," отбрасываются, чтобы очередь не блокировалась, а затем автоматически согласуются плагином (",[189,358,359],{},"planDivergence",", ",[189,362,363],{},"app\u002Fservices\u002Fpowersync\u002FuploadReconcile.ts","):",[203,366,367,376],{},[206,368,369,372,373,323],{},[229,370,371],{},"Отклонённые INSERT",": локальные строки удаляются для сходимости (сервер их так и не принял). Отмена кошелька\u002Fкатегории каскадно удаляет ссылающиеся на них локальные транзакции, поэтому осиротевших строк не остаётся. Toast: ",[189,374,375],{},"sync.errors.uploadReverted",[206,377,378,381,382,385,386,323],{},[229,379,380],{},"Отклонённые UPDATE\u002FDELETE",": на сервере осталась предыдущая версия, а PowerSync не может повторно вытянуть отдельную строку, поэтому пользователю предлагается деструктивная полная повторная синхронизация (",[189,383,384],{},"forceResync",": очистка локальной SQLite + повторная загрузка серверной истины). Toast: ",[189,387,388],{},"sync.errors.uploadDiverged",[335,390,392],{"id":391},"повторяемые-ошибки","Повторяемые ошибки",[173,394,395,396,399],{},"Сетевые ошибки, таймауты и все остальные не-фатальные ошибки вызывают ",[229,397,398],{},"пробрасывание"," ошибки. PowerSync перехватывает его и повторяет попытку с backoff при восстановлении соединения.",[177,401,403],{"id":402},"каскадные-удаления","Каскадные удаления",[173,405,406,409,410,413,414,417,418,421],{},[189,407,408],{},"deleteWallet"," и ",[189,411,412],{},"deleteCategory"," также удаляют транзакции сущности из локальной SQLite до возврата. Это необходимо, поскольку иначе подписка ",[189,415,416],{},"watchTable"," на ",[189,419,420],{},"trns"," вернула бы осиротевшие строки в хранилище после удаления родительской сущности из SQLite. Откат при ошибке локальной записи восстанавливает и саму сущность, и её транзакции.",[173,423,424],{},"Сервер не накладывает ограничений внешних ключей (порядок загрузки PowerSync не гарантирован), поэтому ссылочная целостность обеспечивается логикой приложения.",[177,426,428],{"id":427},"оптимистичный-откат","Оптимистичный откат",[173,430,431,432,435,436,439],{},"Каждая запись в хранилище сохраняет снимок ",[189,433,434],{},"prev = items.value"," до мутации. Если локальная запись в SQLite выбрасывает исключение, восстанавливается ",[189,437,438],{},"items.value = prev"," и показывается toast. Это покрывает ошибки локальной записи (например, ограничение SQLite, нехватка места). Ошибки отказа на сервере (RLS, ограничение) обрабатываются отдельно обработчиком ошибок загрузки.",[177,441,443],{"id":442},"без-ручной-очереди","Без ручной очереди",[173,445,446],{},"Очередью загрузки нативно управляет PowerSync - нет написанной вручную очереди, нет шага replay, нет слияния операций, нет переназначения клиентских ID.",[173,448,449,450,453],{},"Клиентские ID - это обычные UUID (генерируются на клиенте через ",[189,451,452],{},"crypto.randomUUID()"," или аналог). Они стабильны и служат постоянным первичным ключом и на устройстве, и на сервере, поэтому ничего не нужно переназначать после загрузки.",[177,455,457],{"id":456},"ключевые-файлы","Ключевые файлы",[459,460,461,474],"table",{},[462,463,464],"thead",{},[465,466,467,471],"tr",{},[468,469,470],"th",{},"Файл",[468,472,473],{},"Роль",[475,476,477,490,506,521,536,546,556,566,575],"tbody",{},[465,478,479,485],{},[480,481,482],"td",{},[189,483,484],{},"app\u002Fservices\u002Fpowersync\u002Fconnector.ts",[480,486,487,489],{},[189,488,332],{}," - опустошение очереди, разделение фатальных\u002Fповторяемых ошибок",[465,491,492,497],{},[480,493,494],{},[189,495,496],{},"app\u002Fservices\u002Fpowersync\u002Fmutations.ts",[480,498,499,360,502,505],{},[189,500,501],{},"upsertRow",[189,503,504],{},"deleteRow"," - запись в локальную SQLite",[465,507,508,513],{},[480,509,510],{},[189,511,512],{},"app\u002Fservices\u002Fpowersync\u002Fdb.ts",[480,514,515,517,518,520],{},[189,516,416],{}," - подписка на изменения SQLite; ",[189,519,294],{}," - остановить синк, сохранив локальные данные и очередь",[465,522,523,527],{},[480,524,525],{},[189,526,271],{},[480,528,529,532,533],{},[189,530,531],{},"resolveWriteUid"," - откат на персистнутый uid при записи строк; роут-гард ",[189,534,535],{},"hasPersistedSession",[465,537,538,543],{},[480,539,540],{},[189,541,542],{},"app\u002Fapp\u002Fplugins\u002Fpowersync.client.ts",[480,544,545],{},"Регистрирует обработчик ошибок загрузки (toast); ставит синк на паузу (сохраняя данные) при непроизвольной потере сессии",[465,547,548,553],{},[480,549,550],{},[189,551,552],{},"app\u002Fapp\u002Fcomponents\u002Ftrns\u002FuseTrnsStore.ts",[480,554,555],{},"Оптимистичная запись + откат, каскадное удаление trns",[465,557,558,563],{},[480,559,560],{},[189,561,562],{},"app\u002Fapp\u002Fcomponents\u002Fwallets\u002FuseWalletsStore.ts",[480,564,565],{},"Оптимистичная запись + откат + каскадное удаление trns",[465,567,568,573],{},[480,569,570],{},[189,571,572],{},"app\u002Fapp\u002Fcomponents\u002Fcategories\u002FuseCategoriesStore.ts",[480,574,565],{},[465,576,577,582],{},[480,578,579],{},[189,580,581],{},"app\u002Fapp\u002Fcomposables\u002FuseStoreSync.ts",[480,583,584,360,587,590,591],{},[189,585,586],{},"showErrorToast",[189,588,589],{},"showSuccessToast",", demo-only ",[189,592,593],{},"createDebouncedPersist",[177,595,597],{"id":596},"следующие-шаги","Следующие шаги",[203,599,600,606],{},[206,601,602,605],{},[603,604,117],"a",{"href":118}," - потоки синхронизации PowerSync, очередь загрузки и обработка удалений",[206,607,608,610],{},[603,609,108],{"href":109}," - инициализация приложения и паттерн хранилищ",{"title":191,"searchDepth":612,"depth":612,"links":613},2,[614,615,616,617,622,623,624,625,626],{"id":179,"depth":612,"text":180},{"id":197,"depth":612,"text":198},{"id":223,"depth":612,"text":224},{"id":326,"depth":612,"text":327,"children":618},[619,621],{"id":337,"depth":620,"text":338},3,{"id":391,"depth":620,"text":392},{"id":402,"depth":612,"text":403},{"id":427,"depth":612,"text":428},{"id":442,"depth":612,"text":443},{"id":456,"depth":612,"text":457},{"id":596,"depth":612,"text":597},"Как PowerSync обеспечивает офлайн-записи, очередь загрузки и вывод ошибок.","md",null,{},{"icon":125},{"title":122,"description":633},"Финапка работает офлайн-first через PowerSync - локальная SQLite хранит все данные, записи оптимистичны и ставятся в очередь локально, фатальные ошибки загрузки отбрасываются и выводятся через toast.","N5ZM63M4d-Fd7NhSNSLobtrOZenCPusUrCEX8VflPII",[636,638],{"title":117,"path":118,"stem":119,"description":637,"icon":120,"children":-1},"Как PowerSync синхронизирует локальную SQLite с Supabase Postgres.",{"title":127,"path":128,"stem":129,"description":639,"icon":130,"children":-1},"Обоснование ключевых решений.",1782114341025]