Çevrimdışı Çalışabilen Bir İstemci Nasıl Tasarlanır
Bağlantı kesildiğinde uygulamanın çökmemesi ayrı bir problem, o sırada kullanıcının yaptığı değişikliklerin kaybolmaması ayrı bir problemdir. Önbellekleme, iyimser arayüz ve yerel öncelikli mimariyi hangi durumda seçmeniz gerektiğini, kuyruk tasarımını ve çakışma çözümünü anlatıyoruz.
Çevrimdışı demek ne demektir
"Çevrimdışı çalışsın" isteği genelde tek bir problem gibi konuşulur ama aslında ikiye ayrılır. Birincisi okuma tarafı: kullanıcı daha önce gördüğü içeriği bağlantı yokken de görebilsin. İkincisi yazma tarafı: kullanıcı bağlantı yokken bir form doldursun, bir kaydı güncellesin, bir öğe eklesin ve bu işlem kaybolmasın. Bu iki problemin çözümleri birbirinden tamamen farklıdır ve çoğu "offline destek eklemek" girişimi, sadece birinciyi çözüp ikincisini unuttuğu için kullanıcıya yanlış güven verir: arayüz "kaydedildi" der, veri hiçbir yere gitmemiştir.
Aşağıda üç yaklaşımı, hangisinin ne zaman işe yaradığını ve her birinin somut tuzaklarını ele alıyoruz.
Yaklaşım 1: Salt okunur önbellekleme
En basit ve en ucuz çözüm budur. Servis çalışanı (service worker) üzerinden bir "stale-while-revalidate" stratejisi kurarsınız: istek geldiğinde önce önbellekteki cevabı hemen döndürürsünüz, arka planda ağdan taze veriyi çekip önbelleği güncellersiniz. Kullanıcı bağlantısı kesildiğinde en azından son gördüğü sayfayı görmeye devam eder.
Bunun sınırı nettir: yalnızca okuma içindir. Bir form gönderiminde, bir butona tıklamada işe yaramaz, çünkü ortada önbelleğe alınacak bir "cevap" yoktur, sunucuya ulaşması gereken bir "istek" vardır. İçerik siteleri, dokümantasyon, katalog sayfaları gibi büyük çoğunlukla okunan uygulamalarda bu tek başına yeterlidir ve ekstra karmaşıklık gerektirmez.
Buradaki en yaygın tuzak, önbelleğin sürümlenmemesi. Service worker'ın cache adı sabit kalırsa (cache-v1 gibi) ve yeni bir sürüm dağıttığınızda bu ad değişmezse, kullanıcı aylar sonra hâlâ eski JavaScript ve eski API cevaplarını görmeye devam edebilir; hata raporu gelir ama sizin ortamınızda sorun yeniden üretilemez çünkü sizin önbelleğiniz zaten güncel. Her dağıtımda cache adını değiştirip eski cache'leri activate olayında temizlemek bu sınıfın standart çözümüdür.
Yaklaşım 2: İyimser arayüz ve yerel kuyruk
Kullanıcının offline iken bir işlem yapabilmesi gerekiyorsa (bir görev oluşturmak, bir yorum eklemek, bir ayarı değiştirmek) ikinci yaklaşıma geçersiniz. Mantık şöyle işler: kullanıcı işlemi tetiklediğinde arayüz işlemi hemen başarılı gibi gösterir (iyimser güncelleme), asıl istek ise tarayıcının kalıcı depolamasında (IndexedDB, localStorage değil, çünkü senkron çalışır ve ana iş parçacığını bloklar) bir kuyruğa yazılır. Bağlantı geri geldiğinde kuyruk sırayla sunucuya gönderilir.
Bunun iyi çalışması için üç şey şart:
Birincisi, her kuyruk kaydına bir idempotency anahtarı (istemci tarafında üretilmiş bir UUID) eklemek. Ağ isteği zaman aşımına uğrayıp da aslında sunucuya ulaşmışsa, istemci "başarısız oldu" sanıp aynı isteği tekrar gönderebilir. Sunucu tarafı aynı anahtarla gelen ikinci isteği "zaten işlendi" diye tanıyıp yok saymazsa, bir sipariş iki kez oluşur, bir bakiye iki kez düşer.
İkincisi, kuyruğu tetikleyecek bir olay. Tarayıcıların online olayı ve Background Sync API bu iş için var ama Background Sync desteği tarayıcılar arasında tutarlı değil; pratikte hem online olayını hem sayfa tekrar görünür olduğunda (visibilitychange) bir kontrolü hem de basit bir periyodik yoklamayı birlikte kullanmak gerekir. Tek bir tetikleyiciye güvenmek, belirli bir tarayıcıda kuyruğun sonsuza kadar beklemesine yol açar.
Üçüncüsü, kimlik doğrulama süresinin dolması. Kullanıcı uzun süre offline kaldıysa, oturum jetonunun süresi kuyruk boşaltılmadan önce dolmuş olabilir. Kuyruğu boşaltmadan önce jetonu tazelemeyi denemez, doğrudan eski jetonla göndermeye kalkarsanız kuyruktaki tüm istekler 401 ile geri döner ve arayüz zaten "gönderildi" demiş olduğu için kullanıcı bunu asla fark etmez.
Yaklaşım 3: Yerel öncelikli mimari
Birlikte düzenlenen belgeler, tahta uygulamaları, not defterleri gibi işbirlikçi araçlarda ikinci yaklaşım yetmez, çünkü orada tek bir "istek" değil, sürekli akan küçük değişiklikler vardır ve birden fazla kullanıcı aynı veriyi aynı anda değiştirebilir. Burada yerel öncelikli (local-first) mimari devreye girer: veri her zaman önce yerel depoda yaşar, sunucu bir merkezi otorite değil bir eşitleme noktasıdır, ve birleştirme mantığı CRDT (çakışmasız çoğaltılmış veri türleri) ya da operasyonel dönüşüm (OT) ile otomatikleştirilir.
CRDT'ler sayaç, küme, sıralı liste, metin gibi belirli veri yapıları için matematiksel olarak "iki taraf da hangi sırayla birleşirse birleşsin aynı sonuca varır" garantisini verir. Bu, sunucuya danışmadan yazma yapabilmenizi ve daha sonra çakışmasız birleşebilmenizi sağlar. Bedeli karmaşıklıktır: veri modelinizi CRDT'lerin desteklediği yapılara sıkıştırmanız gerekir, silme işlemleri genelde "mezar taşı" (tombstone) kayıtlarıyla temsil edilir ve bu kayıtlar zamanla depoyu şişirir, düzenli temizlenmesi (garbage collection) ayrı bir mühendislik problemidir.
Kuyruktaki bir kaydın taşıması gerekenler
Kuyruğa yazılan her mutasyon en az şu bilgileri taşımalı: bir idempotency anahtarı, işlemin türü, hedef kayıt, gönderilecek veri ve kaç kez denendiği. Basitleştirilmiş bir örnek:
{
"id": "6f2b6e2a-6c31-4e7a-9e2e-2a6f2b6e2a6c",
"type": "update-task",
"resource_id": "task-482",
"payload": { "title": "Rapor gönder", "done": true },
"created_at": "2026-09-09T08:14:00Z",
"attempts": 0
}
Deneme sayısını tutmak önemlidir: sunucu sürekli 400 döndüren bir isteği sonsuza kadar tekrar denemek yerine, üç dört başarısız denemeden sonra kuyruktan çıkarıp kullanıcıya "bu işlem gönderilemedi" diye açıkça göstermek gerekir. Aksi hâlde kuyruk arka planda sessizce büyür, kullanıcı hiçbir şey fark etmez ve bir gün depolama kotası dolduğunda tarayıcı en eski kayıtları kendisi silmeye başlar; hangi işlemlerin kaybolduğunu artık kimse bilemez.
Bunu test ederken gerçek bir ağ kesintisi beklemeyin. Tarayıcıların geliştirici araçlarındaki "offline" kutusu ve yapay gecikme/paket kaybı ayarları, kuyruğun gerçekten dolup dolmadığını, tekrar bağlanınca doğru sırayla boşalıp boşalmadığını ve idempotency anahtarının çift gönderimi gerçekten engelleyip engellemediğini görmenin en hızlı yoludur. Bu senaryoları bir kere elle deneyip geçmek yerine, kuyruk mantığını sunucudan bağımsız, saf birim testlerle de kapsamak gerekir; ağ katmanını taklit eden bir test her zaman gerçek ağdan daha hızlı ve daha tekrarlanabilirdir.
Hangisini ne zaman seçmemeli
Bakiye, ödeme, stok düşme gibi güçlü tutarlılık gerektiren işlemlerde offline yazmaya hiç izin vermemek, bunun yerine net bir "bağlantı kurulana kadar bu işlem yapılamaz" mesajı göstermek çoğu zaman doğru karardır. Buradaki risk teknik değil: kullanıcı iki farklı cihazdan offline sipariş verirse, "iyimser" arayüz her ikisine de "sipariş alındı" der, sunucu ikisini de kabul edemeyebilir, ve bunu kullanıcıya sonradan açıklamak müşteri hizmetlerinin işi olur.
Uygulamanız çoğunlukla ofis içinde, sürekli bağlantılı bir ortamda kullanılıyorsa (bir yönetim paneli, bir dahili araç) offline desteğin getirdiği kuyruk yönetimi, çakışma çözümü ve test yükü kazandırdığından fazlasını götürür. Bu durumda tek yatırım, ağ hatasında kullanıcıya net bir hata mesajı verip veri kaybetmeden formu doldurulu bırakmaktır; bu bile "offline çalışıyor" iddiasından çok daha güvenilir bir deneyimdir.
Doğru soru "offline çalışsın mı" değil, "hangi veri yapısı için, ne kadar tutarlılık kaybına razıyım" sorusudur. Cevap değiştikçe üç yaklaşım arasında farklı katmanlar birleştirmek gerekebilir: okuma için önbellek, kritik olmayan yazmalar için kuyruk, ortak düzenlenen veri için CRDT.