Offline-first PWA dla magazynów: co naprawdę psuje się po utracie sieci

Aplikacja magazynowa to nie blog firmowy. Nie możesz wyświetlić użytkownikowi pustej strony z informacją „sprawdź połączenie z internetem” w momencie gdy stoi z paleta towaru i czeka na wygenerowanie etykiet. W środowisku przemysłowym – hala produkcyjna, magazyn, restauracja – sieć jest niestabilna z natury. WiFi coverage ma dead zones, ktoś wyłączył router żeby zresetować, operator przechodzi między budynkami i zmienia się access point. Przyjęcie towaru, wydanie do restauracji, inwentaryzacja – te operacje muszą działać zawsze.

Brzmi jak klasyczny case dla PWA z offline-first. W teorii Service Worker cache”uje aplikację, IndexedDB trzyma dane, Background Sync wysyła zmiany jak sieć wraca. Proste. Tylko że w produkcji okazuje się, że 90% problematyczne jest w pozostałych 10% przypadków których nie przewidziałeś w happy path.

Pracowałem przy systemie ERP dla gastronomii gdzie offline-first nie był nice-to-have, tylko absolutnym wymogiem. Centralny magazyn, hala produkcyjna, dziesięć restauracji. Każda lokalizacja wykonuje operacje magazynowe – przyjmuje towar, wydaje do produkcji, robi inwentaryzację. System musi wiedzieć co się dzieje wszędzie, ale nie może wymagać stabilnej sieci w każdym momencie. To co wydawało się prostym problemem technicznym szybko przerodziło się w debugowanie edge case”ów typu „operator wykonał 50 operacji offline przez 4 godziny, jak je zsynchronizować bez zniszczenia danych które w międzyczasie zmieniły się online”.

Dlaczego offline-first brzmi dobrze na papierze ale boli w produkcji

Klasyczna aplikacja webowa zakłada że sieć jest. Może być wolna, może timeoutować, ale istnieje. Radzisz sobie spinnerami, retry logic, error handling. Offline-first odwraca to założenie – domyślnie nie ma sieci, online to bonus. Zmienia to fundamentalnie sposób myślenia o architekturze.

W środowisku przemysłowym WiFi jest problematyczne. Magazyn to duży obiekt, często metalowe konstrukcje które bloków sygnał. Dead zones przy regałach, interferencja od innych urządzeń, przeciążone access pointy gdy wszyscy pracownicy logują się rano. Restauracja to jeszcze gorzej – często piwnica, grube ściany, kilka pięter. Zapytałem operatorów ile razy dziennie widzą „no connection” – odpowiedź była „non-stop, przestałem zwracać uwagę”.

Business requirement był jasny: magazynier przyjmuje towarową dostawę – skanuje kody kreskowe, wprowadza ilości, drukuje etykiety. To musi działać niezależnie od sieci, bo dostawca nie będzie czekał pół godziny aż WiFi wróci. Wydanie towaru do restauracji? To samo. Inwentaryzacja? Operator przechodzi przez magazyn z tabletem skanując produkty – pół magazynu może być w dead zone. Nie ma opcji „zrób to później”.

Trade-off jest oczywisty: złożoność. System który działa tylko online to jeden problem – komunikacja z backendem. System offline-first to trzy problemy: komunikacja z backendem, synchronizacja stanów, conflict resolution. Każda operacja którą użytkownik wykonuje offline musi być gdzieś zapisana, musi przetrwać refresh przeglądarki, musi być w odpowiedniej kolejności wysłana do serwera, musi obsłużyć scenariusz że serwer odrzuci operację bo dane się zmieniły. To nie jest coś co się dodaje w dwa dni.

Architektura która naprawdę działa offline

Service Worker to nie jest magiczne rozwiązanie które „robi offline”. To proxy między aplikacją a siecią, który pozwala przechwytywać requesty i decydować skąd wrócić dane. Cache aplikacji to pierwsza oczywista rzecz – statyczne assety (HTML, CSS, JS) muszą być dostępne offline. To działa out-of-the-box gdy używasz typowych cache strategies.

Prawdziwy problem zaczyna się przy danych. Dane magazynowe – stany produktów, pending operations, lokalizacje, użytkownicy – to structured data które muszą być dostępne offline i muszą być synchronizowane online. IndexedDB to jedyna sensowna opcja w przeglądarce dla structured storage (localStorage jest za mały i synchroniczny, Cache API to key-value dla requestów nie dla obiektów). IndexedDB to transactional NoSQL database w przeglądarce – brzmi świetnie dopóki nie zaczniesz go używać.

Największy błąd jaki widziałem to traktowanie IndexedDB jako „local copy of server database”. Wrzucasz tam wszystko co jest na serwerze, aktualizujesz przy każdym fetchu, używasz jako cache. Problem: dane się rozjeżdżają. Użytkownik wykonuje operację offline – modyfikuje lokalny stan. Serwer w międzyczasie dostał update od innego użytkownika. Jak mergować? Która wersja jest prawdziwa?

Podejście które działało: separation of concerns. IndexedDB nie jest kopią server state, jest local state store. Przechowuje trzy typy danych:

Ten podział rozwiązuje problem mergowania – nie masz konfliktu bo nie edytujesz cached server state. Edytujesz local state (pending operations), a serwer merguje to z server state według swoich reguł. Client nie decyduje co jest prawdą, tylko wysyła „chcę wykonać tę operację”, serwer decyduje czy może.

Background Sync API to drugi element układanki. Pozwala zarejestrować sync event który browser wykona jak sieć wróci – nawet jeśli użytkownik zamknął tab. Teoretycznie idealny mechanizm do wysyłania pending operations. Praktycznie: działa tylko w Chrome/Edge, wymaga HTTPS, jest throttled przez browser, nie ma gwarancji wykonania.

W produkcji polegaliśmy na kombinacji: Background Sync jako primary mechanism, ale dodatkowo periodic check w aplikacji (co 30 sekund jeśli jest activity), manual sync button w UI, sync on navigation. Redundancja działa lepiej niż elegancja gdy chodzi o nie gubienie danych użytkowników.

Modelowanie pending operations - jak nie zgubić co użytkownik zrobił

Kluczowe pytanie: jak reprezentować operacje wykonane offline? Pierwsza intuicja: zapisz zmieniony obiekt. Użytkownik zaakceptował dostawę towaru – zapisz obiekt „delivery” ze statusem „accepted”. Problem: co jeśli w międzyczasie serwer zmienił inne pole tego obiektu? Twój update nadpisze serwer state. Co jeśli operacja zależy od innej operacji która też jest offline?

Podejście które działało: operation log, podobne do event sourcing lokalnie. Każda operacja offline to rekord w IndexedDB:

```javascript
{
  id: "uuid",
  operation_type: "accept_delivery",
  payload: {
    delivery_id: "123",
    items: [...],
    location_id: "warehouse_central"
  },
  timestamp: "2024-01-15T10:23:45Z",
  sync_status: "pending",
  idempotency_key: "delivery_123_accept",
  dependencies: [],
  retry_count: 0
}
```

„operation_type” mówi serwerowi co zrobić. Payload zawiera wszystkie dane potrzebne do wykonania operacji. „idempotency_key” zapewnia że ponowne wysłanie tej samej operacji nie zrobi jej dwa razy. „dependencies” to lista innych operation IDs które muszą być wykonane wcześniej.

Dlaczego dependencies? Bo operacje mogą na sobie zależeć. Użytkownik offline przyjął dostawę (utworzył delivery record) i od razu wydał część towaru do produkcji (utworzył issue record referencujący delivery). Issue nie może być wykonane dopóki delivery nie zostanie zapisane na serwerze bo referencuje jego ID. Sync musi respektować kolejność.

Większość operacji magazynowych nie ma dependencies – są niezależne. Przyjęcie towaru w lokalizacji A nie zależy od wydania w lokalizacji B. Ale niektóre mają – zwłaszcza w workflow typu „przyjmij i od razu zużyj”. Dependency graph był minimalny w naszym systemie ale ignorowanie go powodowało błędy typu foreign key constraint violation przy syncu.

„retry_count” to protection przeciw infinite retry loops. Operacja która failuje przy syncu jest retryowana, ale jeśli failuje 5 razy z rzędu, jest oznaczana jako „requires_manual_resolution” i przestaje blockować kolejki. Inaczej jedna zepsuta operacja blokuje wszystkie kolejne.

Status flow: „pending -> syncing -> synced” albo „pending -> syncing -> failed -> pending” (retry) albo „pending -> syncing -> failed -> manual_resolution”. UI pokazuje ten status przy każdej operacji. Użytkownik wie że jego akcja „zaakceptuj dostawę” jest still pending, syncing, albo synced.

"„Conflict resolution” brzmi bardzo elegancko dopóki nie próbujesz wyjaśnić kierownikowi magazynu dlaczego ten sam produkt istnieje jednocześnie w dwóch różnych stanach magazynowych. Rozproszone systemy potrafią być zaskakująco kreatywne w interpretowaniu rzeczywistości."

Sync strategy - kiedy i jak wysyłać dane

Naiwne podejście: jak tylko sieć wróci, wyślij wszystkie pending operations jednym requestem. Problem: co jeśli jest ich 200? Co jeśli część się uda a część nie? Co jeśli timeout w połowie?

Network detection to pierwszy problem. Browser events „online”/”offline” są unreliable – często mówią że jesteś online gdy faktycznie nie masz internetu (jesteś podłączony do WiFi ale WiFi nie ma WAN). Używaliśmy kombinacji: słuchanie „online” event + periodic ping do naszego backend health endpoint (co 30 sekund). Dopiero gdy ping wrócił 200 uznawaliśmy że faktycznie jesteśmy online.

Sync trigger: kilka źródeł. Background Sync event (jeśli browser go wywoła). Online event (z weryfikacją pingiem). Periodic check jeśli jest activity (user robi coś w appce). Manual sync button (user kliknie „synchronizuj teraz”). Redundancja zapewnia że sync się wydarzy nawet jeśli jeden mechanizm zawiedzie.

Batch vs incremental sync: wysyłać wszystkie pending operations jednym requestem czy po jednym? Batch jest szybszy (jeden roundtrip), ale all-or-nothing przy błędzie. Incremental jest wolniejszy ale pozwala na partial success. Używaliśmy hybrid approach: batch po 10 operacji. Jeśli batch failuje, fallback do incremental dla tego batcha. 90% przypadków sync jest szybki (mało operacji), 10% gdzie jest dużo operacji ma protection.

Partial failures to rzeczywistość. Operacja A się udała, B failnęła, C nie została jeszcze wysłana. Jak to obsłużyć? Nie możesz rollbackować A bo już zmieniła server state. Używaliśmy per-operation status: każda operacja ma swój status niezależnie. Failed operation jest retryowana ale nie blokuje kolejnych (chyba że są dependencies). UI pokazuje która operacja failed, user może zobaczyć error i zdecydować co zrobić (retry, skip, manual fix on server).

Sync kolejność: FIFO z respektowaniem dependencies. Operacje są sortowane po timestamp, przetwarzane kolejno, ale jeśli operacja ma unfulfilled dependency, jest skipowana i wraca na koniec kolejki. To zapewnia że dependency A zostanie przetworzona przed B nawet jeśli obie są pending.

Conflict resolution - co gdy dwie wersje prawdy się spotykają

Konflikty powstają gdy dwóch użytkowników edytuje ten sam zasób. User A offline zmienia stan produktu, User B online zmienia ten sam stan. Jak serwer ma to zmergować?

W systemie magazynowym większość operacji to nie edycja istniejących rekordów, tylko tworzenie nowych zdarzeń. Przyjęcie towaru to nowy rekord delivery, nie edycja produktu. Wydanie to nowy issue record. To minimalizuje konflikty – concurrent creates rzadko konfliktują (chyba że jest constraint violation typu unique key).

Konflikty pojawiają się przy: inwentaryzacji (korekta stanu), edycji danych produktu, edycji zamówienia. To są operacje które modyfikują istniejący rekord.

Strategia conflict resolution zależy od typu operacji:

W praktyce konflikty były rzadkie – poniżej 1% operacji offline. Większość użytkowników pracuje na różnych produktach w różnych lokalizacjach. Ale ten 1% musiał być obsłużony bo czasem to były krytyczne operacje.

Używaliśmy vector clocks w uproszczonej formie – każdy client ma ID, każda operacja ma client ID + local sequence number. Serwer może wykryć czy dwie operacje były concurrent (różne client IDs, overlapping timestamps) czy sequential. Concurrent operations to potencjalny konflikt, sequential to prosta kolejność.

Selective data sync - nie pobieraj tego czego nie potrzebujesz

Pierwotny pomysł był prosty: zsynchronizuj cały stan magazynu offline. Wszystkie produkty, wszystkie stany, wszystkie lokalizacje. User zawsze ma pełne dane. Problem: to są dziesiątki tysięcy rekordów, setki megabajtów. IndexedDB teoretycznie może to unieść, praktycznie browser w pewnym momencie zaczyna throttlować albo wyrzuca quota exceeded.

Selective sync: pobieraj tylko dane relevantne dla tego usera w tym momencie. Magazynier w centralnym magazynie nie potrzebuje danych z restauracji. Pracownik restauracji nie potrzebuje danych z hali produkcyjnej. Context-aware sync redukuje ilość danych 10x.

Location-based filtering: user wybiera lokalizację w której pracuje (albo system wykrywa po GPS/WiFi SSID). Pobierane są tylko produkty dostępne w tej lokalizacji, tylko operacje dotyczące tej lokalizacji. Zmiana lokalizacji triggeruje resync.

Lazy loading dla rzadkich danych. Product catalog jest pobierany, ale full history każdego produktu nie. Jeśli user chce zobaczyć historię – fetch on demand online, albo pokazanie „not available offline”. Trade-off: nie wszystko jest offline, ale to co jest to rzeczywiście potrzebne.

Cache invalidation to klasyczny problem. Dane w IndexedDB szybko stają się stale. Używaliśmy TTL per data type: product data – 1 godzina, inventory levels – 5 minut, user permissions – 1 dzień. Expired data jest refetchowane on next online, ale wciąż pokazywane offline (stare dane lepsze niż żadne). UI pokazuje timestamp „last synced: 2h ago”.

Problem: co jeśli user pracuje na stale danych? Może wykonać operację która jest invalid w current state (np. wydać produkt który już został wydany przez kogoś innego). Serwer odrzuci taką operację przy syncu. Validation przy syncu sprawdza constraints i zwraca descriptive error. User dostaje info „operacja X nie mogła być wykonana bo produkt już niedostępny” i może manualnie poprawić.

"Najpierw Redis był kolejką, potem cache’em, potem lock managerem, a przez chwilę prawie religią. Dopiero Postgres musiał przypomnieć wszystkim, że source of truth powinien przeżyć więcej niż chwilowy kaprys procesu."

UI/UX offline mode - user musi wiedzieć w jakim stanie jest system

Najgorszy UX to when user nie wie czy jest online czy offline. Wykonuje operację, nie wie czy się zapisała, czy czeka na sync, czy failnęła. W systeme magazynowym to jest krytyczne – user musi wiedzieć czy etykieta się wydrukowała, czy przyjęcie towaru jest w systemie.

Offline badge zawsze visible. Górny róg ekranu, persistent indicator: „Online” (zielony), „Offline” (pomarańczowy), „Syncing…” (niebieski). User jednym spojrzeniem wie w jakim trybie pracuje.

Per-operation sync status przy każdej operacji. Accepted delivery pokazuje icon: pending (zegar), syncing (spinner), synced (checkmark), failed (warning). Kliknięcie w icon pokazuje details. User może zobaczyć że jego 5 operacji z rana już się zsyncowały ale ta jedna z przed chwili jeszcze czeka.

Optimistic UI updates z możliwością rollbacku. Jak user kliknie „accept delivery”, UI natychmiast pokazuje że delivery został zaakceptowany. Operacja jest dodana do pending queue. Jeśli sync się uda – great. Jeśli fail – UI pokazuje error i rollbackuje visual state. To jest lepsze niż blocking UI dopóki sync się nie skończy, ale wymaga careful state management.

Informowanie o konfliktach to osobny challenge. Nie możesz wywalić alertu „konflikt w operacji 12345” bo user nie rozumie. Używaliśmy notification center: icon z licznikiem nierozwiązanych konfliktów, kliknięcie otwiera listę z human-readable opisami „Twoja korekta stanu produktu X konfliktuje z wydaniem które wykonał User Y”. User może to addressować jak ma czas.

Manual sync button był surprisingly często używany. Mimo że auto-sync działał, users lubili mieć kontrolę. Przed końcem zmiany klikali „sync now” żeby upewnić się że wszystko poszło. Button pokazywał progress: „syncing 5/12 operations…”. Dawało to peace of mind.

Error messages musiały być actionable. Zamiast „sync failed” -> „Operacja przyjęcia towaru nie mogła być zapisana: produkt ABC123 nie istnieje w systemie. Sprawdź kod produktu.” User wie co jest nie tak i może to naprawić (albo zgłosić do supportu z konkretnym info).

Storage management - IndexedDB się zapełnia

IndexedDB ma quota, która różni się per browser i zależy od available disk space. Chrome daje ~60% available disk, Firefox mniej. Dla aplikacji z dużą ilością offline operations quota może być problemem.

Symptoms: user dostaje błąd „QuotaExceededError” przy próbie zapisania operacji. Najgorszy moment – jest offline, wykonuje krytyczną operację, aplikacja mówi „nie mogę zapisać”. To jest unacceptable.

LRU eviction dla starych danych. Regularny cleanup job (uruchamiany przy app start albo w background) który usuwa synced operations starsze niż 7 dni. Nie usuwamy pending ani failed operations – tylko te które są już safely on server. To redukuje storage usage bez ryzyka.

Manual cleanup przez usera. Settings -> Storage: pokazujemy ile jest used, opcja „clear old data”. User może manualnie wyczyścić jeśli zbliża się do limitu. Nie jest to eleganckie ale działa.

Monitoring storage usage. Przy każdym zapisie do IndexedDB checkujemy quota: „navigator.storage.estimate()”. Jeśli zostało <10% – warning w UI „Pamięć aplikacji jest prawie pełna, rozważ wyczyszczenie starych danych”. Proaktywne info zamiast czekania na QuotaExceededError.

Problem: co jeśli user ignoruje warning i quota jest exceeded? Fallback strategy: próbujemy usunąć oldest synced operations automatycznie. Jeśli to nie pomaga – oznaczamy operację jako „saved to file” i zapisujemy JSON do Downloads. User może potem ręcznie uploadować. To jest hacky ale lepsze niż total data loss.

IndexedDB corruption to edge case który się zdarza. Browser crash w trakcie write, disk full, inne patologie. Symptoms: otwieranie database failuje, queries throwują enigmatyczne errory. Nie ma dobrego recovery – jedyne co możesz to delete całego database i zacząć od nowa. Używaliśmy try-catch przy każdym database open, jeśli failnęło więcej niż raz – pokazujemy error „Database corrupted, reset required?” z opcją reset. User traci pending operations (hopefully miał ich mało) ale aplikacja znowu działa.

Edge cases które uczepiły nas w produkcji

Long offline period: User był na urlopie 5 dni, wraca, ma 300 pending operations. Sync trwa 15 minut, w trakcie którego UI jest sluggish (bo każda operacja jest validowana i recorded). Fix: pokazywaliśmy dedicated „syncing” screen z progress barem przy długim syncu (>50 operations). User mógł zminimalizować i robić coś innego, ale wiedział że sync się dzieje.

Operation validation fail przy sync: User offline wydał produkt który w międzyczasie wygasł (expired). Serwer odrzuca operację przy syncu. Fix: serwer zwraca detailed validation error, UI pokazuje „nie można wydać produktu X – wygasł dnia Y”, operacja jest marked jako failed z możliwością manual resolution.

Dependency deadlock: Operation A zależy od B, B zależy od A (błąd w logice tworzenia operations). Sync loop nigdy się nie kończy. Fix: dependency validation przed zapisem operacji – cycle detection. Jeśli wykryjemy cycle – reject operacji z errorem. Defensive programming bo to nie powinno się zdarzyć, ale się zdarzyło.

Service Worker cache stale dane: Service Worker cache zwrócił starą wersję API response. User widzi outdated data jako „current”. Fix: cache busting przez versioned URLs, aggressive cache invalidation dla data requests (network first z fallbackiem na cache, nie cache first).

Browser wyczyszcił IndexedDB: User w privacy mode albo browser automatycznie wyczyszczone storage (niektóre mobile browsers są aggressive). User traci pending operations. Fix: detect database empty on app start, pokazanie warning „Database was cleared, offline operations may be lost”. Backup operations do localStorage jako secondary storage (mniejszy limit ale mniej likely do wyczyścić).

Concurrent tabs: User ma dwa taby aplikacji otwarte. Obie próbują syncować pending operations. Race condition – ta sama operacja wysłana dwa razy. Fix: idempotency keys zapewniają że duplicate nie robi harm, ale też używaliśmy BroadcastChannel API żeby sync w jednym tabie informował drugi tab „I”m syncing, stay back”.

Testing offline behavior - jak nie robić tego manualnie w kółko

Testowanie offline jest pain. Manualny flow: otwórz DevTools, zaznacz „Offline”, wykonaj operacje, odznacz „Offline”, sprawdź czy sync działa. Repeat 100 razy.

Chrome DevTools ma symulację offline/slow 3G/fast 3G. Używaliśmy tego dla manual testing ale też dla automated. Playwright/Puppeteer pozwala na emulację network conditions w testach:

```javascript
await page.context().route('**/*', route => route.abort()); // simulate offline
```

Service Worker lifecycle testing: SW może być w stanie installing, waiting, active. Trzeba testować update flow – co się dzieje gdy user ma starą wersję SW, pojawia się nowa, user jest w trakcie offline operations. Test: simulate update, verify pending operations nie są lost.

Automated tests dla sync scenarios: test który tworzy pending operations w IndexedDB, symuluje network, triggeruje sync, veryfikuje że operations zostały wysłane w poprawnej kolejności i status został updated. To jest integration test (wymaga backend stub) ale catch najwięcej bugs.

Chaos engineering dla offline: random network failures w trakcie sync. Połowa requestów failuje, sprawdzamy czy retry logic działa, czy partial success jest properly handled, czy UI pokazuje poprawny status. To wykryło wiele edge cases których normalny testing nie złapał.

"Najbardziej stabilnym elementem offline-first jest zwykle przekonanie, że przetestowałeś już wszystkie edge case’y. Produkcja potem uprzejmie pokazuje, że użytkownik z dwoma tabami, starym Service Workerem i pełnym IndexedDB miał inne plany."

Monitoring i debugging w produkcji

Największy problem z offline-first: jak debugować coś co działo się na urządzeniu usera bez internetu? Nie ma live logs, error tracking typu Sentry nie działa offline.

Client-side error logging z offline queue. Każdy error (JS exception, failed operation, conflict) jest zapisywany lokalnie z full context (timestamp, user, operation, stack trace). Jak sieć wraca – wysyłany do logging backend. To dało nam visibility co się dzieje w terenie.

Metrics które zbieraliśmy: sync success rate (ile operacji sync/ile total), conflict rate (ile konfliktów/ile operacji), time spent offline (aggregate per user), quota usage (ile IndexedDB space używane). To pokazywało bottlenecks i problemy.

Sync success rate był naszym primary health metric. Powyżej 95% – OK. Poniżej – investigation. Spadki korelowały z deployment błędów albo problemami z backend.

User-reported issues z możliwością export pending operations. User miał opcję „report problem” która generowała JSON dump pending operations + storage state + logs. Mógł to wysłać do supportu. To bardzo pomogło w debugowaniu „sync nie działa” – widzieliśmy exact state aplikacji.

Praktyczne wnioski

To nie jest teorię ani best practices. To jest zbiór rzeczy które nie działały pierwszym razem, zostały naprawione po debugowaniu produkcji, i uformowały się w architekturę która – more or less – działa. Offline-first brzmi jak dobry pomysł dopóki nie zaczynasz myśleć o wszystkich sposobów w które concurrent operations mogą się konfliktować, IndexedDB może się zapełnić, albo browser może zdecydować że Twoje dane są nieważne. Produkcyjny system offline-first to głównie defensive programming przeciwko założeniu że cokolwiek będzie działać reliably.

Największa lekcja: nie projektujesz pod happy path gdzie user idzie offline na minutę i wszystko syncuje się magicznie. Projektujesz pod scenariusze gdzie user jest offline cztery godziny, wykonał setkę operacji, z których połowa konfliktuje z tym co się stało online, a IndexedDB właśnie się zapełnił. Jeśli system przeżyje ten chaos – będzie działać w normalnych warunkach. Większość problemów z offline-first to nie technical elegance, tylko brutal reality verification że distributed state is hard i browsers weren”t really built for this.

Podsumowanie

Offline-first brzmi świetnie na diagramach architektury. Potem przychodzi produkcja, operator przechodzi między regałami, WiFi znika dokładnie w połowie operacji, a browser zaczyna podejmować własne decyzje dotyczące tego, które dane jeszcze „warto” trzymać lokalnie. Bardzo szybko okazuje się, że większość problemów nie dotyczy synchronizacji danych, tylko synchronizacji założeń między backendem, frontendem i rzeczywistością hali magazynowej.

Najbardziej zdradliwe były rzeczy, które teoretycznie „nie powinny się wydarzyć”. Dwie zakładki próbujące syncować te same operacje, IndexedDB kończące miejsce dokładnie przy krytycznym wydaniu towaru, albo user wracający po kilku godzinach offline z kolejką operacji opartą o dane, które od dawna nie istnieją po stronie serwera. Distributed systems są bardzo kreatywne w znajdowaniu scenariuszy, których nikt nie uwzględnił na stagingu, szczególnie gdy działają wewnątrz przeglądarki i zależą od jakości magazynowego WiFi.

Po pewnym czasie przestajesz budować „offline mode”, a zaczynasz budować system odzyskiwania po chaosie. Retry logic, idempotency keys, dependency graphs, manual conflict resolution i defensive programming przestają być dodatkiem do architektury, a stają się właściwą aplikacją. Produkcja dość brutalnie uczy, że eventual consistency brzmi profesjonalnie tylko do momentu, aż ktoś zadzwoni z pytaniem, dlaczego stan magazynowy właśnie stał się liczbą losową.

Zobacz powiązane case studies i analizy

Procesy, architektura i workflow powiązane z tematami poruszanymi w tym materiale – od integracji i realtime systems po automatyzacje operacyjne.

Inne

Pozostałe artykuły

Webhook Reliability Patterns: czego nauczyłem się debugując phantom calls w systemie VoIP
Distributed state w systemach realtime działa poprawnie tylko do momentu, w którym różne warstwy infrastruktury zaczynają posiadać sprzeczne informacje o tym samym połączeniu.
Jak dostarczyć MVP w 14 dni bez tworzenia architektonicznej katastrofy, która zablokuje rozwój produktu po pierwszym sukcesie?