Offline-First і Background Sync (PWA)
Коротко: Дії користувача (наприклад, «додати в кошик») за відсутності інтернету не відкидаються, а зберігаються локально в браузері і автоматично відтворюються, щойно з’єднання повернеться.
Докладніше: Використовується IndexedDB (замість localStorage), бо service worker має мати до нього доступ у фоні, навіть коли сама сторінка взагалі не відкрита — localStorage недоступний із контексту service worker. API Background Sync (лише Chrome/Edge/Android) дає змогу service worker запускати відтворення навіть за закритої сторінки; як універсальний запасний варіант (працює і в Safari/iOS) додатково використовується простий слухач події браузера 'online'.
Наш контекст: app/utils/offlineQueue.ts в Emzett (CART_QUEUE_STORE) для кошика — з ідемпотентною синхронізацією через /api/cart/sync, щоб повторна спроба синхронізації (Background Sync І подія 'online') не створювала дубльованих записів.
Подробиці
Ідемпотентність як ключова вимога
«Ідемпотентний» — ключове слово: оскільки і API Background Sync, і запасний варіант із подією 'online' можуть запускати ту саму синхронізацію (теоретично обидва можуть навіть спрацювати одночасно, якщо браузер виходить у мережу саме в цей момент), сервер потрібно побудувати так, щоб дворазове надсилання тієї самої дії НЕ давало подвійного ефекту — зазвичай через унікальний ідентифікатор, що створюється клієнтом для кожної дії (найчастіше UUID, згенерований локально під час створення дії), який сервер упізнає за вдруге і ігнорує, замість того щоб додати товар у кошик удруге. Цей ідентифікатор має бути створений ДО самої синхронізації (а не лише під час надсилання), щоб лишатися однаковим за кожної спроби синхронізації — якби за кожної спроби створювався новий ідентифікатор, перевірка ідемпотентності була б неефективна.
Типовий процес
Користувач натискає «додати в кошик» без з’єднання → дія одразу потрапляє в чергу IndexedDB І оптимістично відображається в інтерфейсі, ніби вже пройшла → щойно navigator.onLine знову стає true (або service worker пробуджується через Background Sync), обробник синхронізації по порядку обробляє чергу й надсилає кожну дію на сервер → за успіху запис черги видаляється, за невдачі зберігається для пізнішої спроби. Оптимістичне оновлення інтерфейсу — свідомий вибір: користувач, що робить покупки офлайн, не повинен відчувати, що застосунок «зламаний» — натомість дія одразу виглядає успішною, з ненав’язливою підказкою («буде синхронізовано, коли ви знову будете онлайн»), якщо щось справді не вдалося.
self.addEventListener("sync", (event) => {
if (event.tag === "cart-sync") {
event.waitUntil(processQueuedCartActions());
}
});Чому IndexedDB, а не localStorage
localStorage синхронний і ненадовго блокує основний потік, що за великих обсягів даних може спричиняти помітні підгальмовування; IndexedDB же асинхронний і розрахований на структуровані, великі обсяги даних (сховища об’єктів з індексами, транзакції). Але вирішальний момент тут інший: localStorage прив’язаний до основного потоку документа і просто недоступний із контексту service worker — service worker працює у власному потоці, незалежному від документа, саме щоб працювати й тоді, коли сама сторінка взагалі не відкрита. IndexedDB — один із небагатьох API зберігання, однаково доступних з обох контекстів (документа І service worker).
Обмеження API Background Sync
API Background Sync навмисно не гарантує негайне виконання — браузер сам вирішує, коли реально запустити синхронізацію (залежно від якості мережі, рівня заряду тощо), але зазвичай протягом кількох секунд після перепідключення. Підтримка браузерами теж нерівномірна: Safari (а отже, і всі браузери на iOS, бо Apple примусово використовує там власний рушій WebKit) досі не підтримує цей API — тому запасний варіант із подією 'online' — не необов’язкове доповнення, а суворо необхідний для міжплатформної надійності.
Див. також: proxy.ts, Протокол без встановлення з’єднання