Yerel Öncelikli Web Geliştirmenin Mimarisi Windows

Yerel Öncelikli Web Geliştirmenin Mimarisi

Yerel Öncelikli Web Geliştirmenin Mimarisi...

Yerel Öncelikli Web Geliştirmenin Mimarisi

Twitter'da paylaş, LinkedIn

2026'da yerel öncelikli web uygulamaları geliştirmek için gerçekte ne gerekiyor? Bu işi sihirli değneklere şüpheyle yaklaşacak kadar uzun süredir yapan geliştiriciler için sağlam, deneyim odaklı bir bakış açısı. 2026'da yerel öncelikli web uygulamaları geliştirmek için gerçekte ne gerekiyor? Bu işi sihirli değneklere şüpheyle yaklaşacak kadar uzun süredir yapan geliştiriciler için sağlam, deneyim odaklı bir bakış açısı.

Sistem Güvenliği

Geçen Ekim ayında, ekibimin dört ay boyunca geliştirmek için harcadığı bir proje yönetimi aracının tanıtımını yapmamdan önceki gece, Lizbon'da bir otel odasında oturuyordum. Otelin Wi-Fi'si bağlandığı yerde o şeyi yapıyordu ama aslında hiçbir şey yüklenmiyor. Ve uygulamamızın, gerçekten gurur duyduğum bu şeyin, döndürücüyle boş bir ekran oluşturmasını izledim. Daha sonra zaman aşımı hatası. Sonra hiçbir şey.

Önemli Gelişmeler

Telefonumu çıkardım, hücresel ağa bağlandım ve bağlantım zayıfladı. Uygulama yüklendi ancak her tıklama iki saniyelik bir bekleme anlamına geliyordu. Bir görev oluşturulsun mu? Döndürücü. Bir görev sütunlar arasında mı taşınsın? Döndürücü. Orada oturup düşündüm: React'ta bir ön uç, Node'da bir arka uç, bir Postgres veritabanı, bir Redis önbelleği, yalnızca görev panosu için altı çözümleyicili bir GraphQL API oluşturduk. Tüm bu altyapı ve kahrolası şey, 3.000 mil uzaktaki bir sunucuya gidiş-dönüş olmadan bana kendi verilerimi gösteremez.

O gece, yerel öncelikli mimariyi ciddi olarak incelemeye başladım. Bir blog yazısı okuduğumdan ya da bir tweet gördüğümden değil. Çünkü utanıyordum.

Bir konuda açık konuşmak istiyorum: İlk yılımı akademik olarak yerel öncelikliliği bir kenara bırakarak geçirdim. Ink & Switch'in "Yerel Öncelikli Yazılım" makalesini 2019'da çıktığında okudum ve "Harika bir araştırma, gerçek uygulamalar için pratik değil" diye düşündüm. Yanılmışım. 2019'daki takımlar gerçekten hazır değildi. Ama aynı zamanda tembellik de yapıyordum, zaten bildiğim mimariyi varsayılan olarak kullanıyordum. Belgede yazılım için yedi ideal ortaya konuldu: hızlı, çok cihazlı, çevrimdışı, işbirliği, uzun ömürlülük, gizlilik, kullanıcı sahipliği. Ve bunların mühendislik gereksinimleri değil de istek listesi gibi göründüğünü düşündüğümü hatırlıyorum.

Sonuç ve Değerlendirme

Yedi yıl sonra, yerel öncelikli kalıpları kullanarak üç üretim uygulaması gönderdim. Ayrıca yanlış karar olduğu iki projeden yerel öncelikli olanı da çıkardım. Görüşlerim var. Bazıları muhtemelen yanlıştır. Ama bunlar kazanıldı.

İşte 2026'da yerel öncelikli web uygulamaları oluşturma konusunda aslında düşündüklerim bunlar, bu işi sihirli değneklere şüpheyle yaklaşacak kadar uzun süredir yapan geliştiriciler için yazılmış.

Teknik Analiz

Ön uç, tasarım ve kullanıcı deneyimine ilişkin yararlı ipuçları içeren Çarpıcı E-posta Bülteni ile tanışın. Abone olun ve neredeyse her şeyi tasarlarken ve oluştururken kendinize sormanız gereken 150'den fazla soru içeren ücretsiz bir PDF destesi olan "Akıllı Arayüz Tasarımı Kontrol Listeleri"ni edinin. (Parçalayıcı) e-postanız

Detaylar ve Etkileri

“Önce Yerel” Aslında Ne İfade Ediyor (Ve Ölmeyecek Karışıklık)

Gelecekte Ne Bekleniyor?

Bir şeyi açıklığa kavuşturmam gerekiyor çünkü bu konuşmayı buluşmalarda yapmaya devam ediyorum. Önce yerel, önce çevrimdışı değil. Bu, "bir servis çalışanı ekleyin ve bir gün sonra arayın" değil. PWA'nın eşanlamlısı değil. Bunların hepsini konferans konuşmalarında bir arada gördüm ve bu beni biraz deli ediyor.

Çevrimdışı öncelikli, uygulamanızın ağ kaybını incelikle ele aldığı anlamına gelir, ancak sunucu hala gerçeğin kaynağıdır. Ağ geri geldiğinde sunucu kazanır. Önbellek öncelikli (hizmet çalışanlarının yanıtları önbelleğe alması) bir performans optimizasyonudur. Eski verileri daha hızlı sunuyorsunuz, bu harika, ancak verilerin sahibini değiştirmediniz. PWA'lar bir dağıtım mekanizmasıdır: kurulabilir, önbelleğe alınmış, anında bildirimler. Bunların hiçbiri veri mimarisi değil.

Uzmanların Görüşleri

Yerel öncelikli bir veri mimarisidir. Kullanıcınızın cihazı, verilerinin birincil kopyasını tutar. Uygulama yerel bir veritabanını okur ve yazar. Anında oluşturulur. Arka planda sunucularla veya diğer cihazlarla senkronize edilir. Sunucu, mevcut olduğunda, bazı özel yetkilere (kimlik doğrulama, yedekleme, erişim kontrolü) sahip bir senkronizasyon eşidir. Ama bu kapı bekçisi değil.

Ink & Switch makalesi yedi ideal tanımladı ve bence bunlar hala geçerliliğini koruyor. Ancak pratikte en önemli olan, her şeyi nasıl inşa ettiğinizi değiştiren şey şudur:

Nasıl Önlem Alınmalı?

İstemci, verileri göstermek için izin isteyen ince bir görünüm değildir. İstemci, kendi veritabanına sahip, dağıtılmış bir sistemdeki bir düğümdür.

Bu ayrım kulağa çok ince geliyor. Değil. Tüm yığınınızı değiştirir.

Erkenden Dürüst Olun: Bunu Yapmamanız Gerektiğinde

Bunu en üste koyuyorum çünkü çok sayıda geliştiricinin (bir zamanlar ben de dahil olmak üzere) yeni bir mimari konusunda heyecanlandığını ve onu ait olmadığı projelere sürüklediğini gördüm. Yaklaşık altı tanesini boşa harcadım Önceki bir işteki dahili analitik kontrol paneli için yerel öncelikli bir yaklaşımın işe yaramasını sağlamak için haftalarca uğraştım. Meslektaşım Sarah sonunda beni kenara çekti ve şöyle dedi: "Veriler sunucuda oluşturuluyor. İstemciye kopyalanacak hiçbir şey yok. Ne yapıyorsun?" Haklıydı.

Verileriniz öncelikle sunucu tarafından oluşturulduğu zaman, yerel öncelik kötü bir uyumdur. Analitik kontrol panelleri, sosyal medya yayınları, arama sonuçları: Sunucu bu verileri üretir, dolayısıyla istemcinin bu verileri API istekleri yoluyla tüketmesi tamamen sorun olmaz.

Güçlü işlem tutarlılığına ihtiyaç duyan sistemler için bu yanlıştır. Bankacılık, ödeme işlemleri ve envanter yönetimi. İki kişi stoktaki son ürünü satın almaya çalışırsa, bu kararı ACID garantileriyle veren tek bir yetkili veritabanına ihtiyacınız vardır. Nihai tutarlılık size para kaybettirecek veya daha kötüsü olacaktır.

Çevrimdışı veya işbirliği ihtiyacı olmayan basit CRUD uygulamaları için bu çok fazla. İnterneti iyi olan bir ofiste beş kişi tarafından kullanılan dahili bir yönetici paneli oluşturuyorsanız, senkronizasyon motoru eklemek aşırı mühendislik gerektirir. Ayrıca istemci cihazlarına sığmayan büyük veri kümeleri için fiziksel olarak pratik değildir.

Ancak işin parladığı yer burası: not alma, belge düzenleme, işbirlikçi tasarım araçları, proje yönetimi, güvenilmez bağlantılara sahip saha uygulamaları, temel olarak veri gizliliğinin satış noktası olduğu her şey ve gerçek zamanlı işbirliğine sahip her şey. Başka bir deyişle, anlık etkileşimden yararlanan ve sunucunun çökmesi durumunda hayatta kalması gereken, kullanıcı tarafından oluşturulan veriler için harikadır.

Keşke birisinin bana daha önce söylemesini istediğim bir şey daha var: Her şeyi yapmak zorunda değilsin. Geleneksel uygulamalardaki belirli özellikler için önce yereli kullanarak en iyi sonuçları elde ettim. Bir blog düzenleyicide çevrimdışı taslaklar. Normalde standart olan REST proje yönetimi aracının içindeki gerçek zamanlı işbirliğine dayalı notlar.

İstekler Değil Kopyalar

Git'i kullandıysanız zihinsel modeli zaten anlamışsınızdır.

SVN (SVN'yi hatırlıyor musunuz?) merkezileştirildi. Bir sunucu. Dosyaları teslim alırsınız, değişiklikler yaparsınız ve sunucuya taahhütte bulunursunuz. Sunucu kapalı mı? Taahhüt edilemiyor. Tarihi bile göremiyorum.

Git her geliştiriciye tam bir klon verdi. Yerel olarak taahhütte bulunursunuz, yerel olarak dallanırsınız ve yerel olarak birleşirsiniz. Hazır olduğunuzda itin ve çekin. Uzak depo önemlidir ancak gerçeğin tek kopyası değildir.

Yerel öncelikli web geliştirme, uygulama verileri için Git'tir. Her istemci cihaz, ilgili verilerin bir kopyasını (tam veya kısmi) tutar. Yazmalar yerel olarak gerçekleşir. Senkronizasyon arka planda itme/çekme şeklindedir. Çatışmalar, tanımlanmış birleştirme stratejileri aracılığıyla çözülür.

Bunun benim için pratikte ilk kez işe yaradığı zamanı hatırlıyorum. Bir görev panosunun prototipini yapıyordum ve görev eklemek için bir fonksiyon yazdım. Eski mimarimizde şöyle olurdu:

API'ye POST. Yanıtı bekleyin. Başarılı olursa yerel durumu güncelleyin. Başarısızlık durumunda, hata bildirimini gösterin ve belki de iyimser güncellemeyi geri alın.

Yerel ilk sürümde şuydu: yerel SQLite'a yazma işlemi tamamlandı. Kullanıcı arayüzü aynı yerel veritabanından okunduğu için anında güncellendi. Senkronizasyon her zaman gerçekleşti. Yükleme durumu yok, yazmanın kendisi için hata işleme yok, iyimser güncelleme mantığı yok (çünkü "iyimser" olunacak hiçbir şey yok; yerel yazma durumdur).

Etkileri her şeye dalga dalga yayılıyor. Veri almak için React Query'ye veya SWR'ye ihtiyacınız yok çünkü veri getirmiyorsunuz. Sunucudan türetilmiş durum için Redux veya Zustand'a ihtiyacınız yoktur çünkü yerel veritabanı sizin durumunuzdur. Yönlendirmeniz API çağrılarını tetiklemiyor. Kimlik doğrulama farklı çalışır çünkü sunucu her okumada izinleri kontrol etmez.

Eğer (benim gibi) mekânsal düşünen biriyseniz işinize yarayabilecek görsel bir karşılaştırma:

Sol tarafta, her kullanıcı etkileşimi bir gidiş-dönüştür. Tıkla, bekle, oluştur. Sağda, okuma ve yazma işlemleri doğrudan yerel veritabanına çarpıyor. Senkronizasyon sunucusu hâlâ oradadır ancak arka planda işini yapmaktadır. Kullanıcı asla bunu beklemez. Temel değişim budur.

Ama kendimin önüne geçiyorum. Senkronizasyon ve çakışmalardan bahsetmeden önce, verilerin gerçekte istemcide nerede bulunduğundan bahsetmemiz gerekir.

Verilerin İstemcide Bulunduğu Yer

localStorage'ı unutun. Senkrondur (ana iş parçacığını engeller), 5-10 MB boyutundadır ve yalnızca dizeleri depolar. Bir tema tercihi için sorun değil. Bu bir veritabanı değil.

IndexedDB kimsenin sevmediği güçlü bir üründür. Her tarayıcıda bulunur, eşzamansızdır, yüzlerce megabaytı işleyebilir ve API'siyle çalışmak kesinlikle berbattır. Toplamda bir kez doğrudan kullandım. Şimdi onu soyutlamalar veya daha fazlası yoluyla kullanıyorum çoğu zaman hiç kullanmıyorum.

Çünkü 2026'daki asıl hikaye SQLite'ın WebAssembly aracılığıyla tarayıcıda çalıştırılmasıdır.

Bunun bir parti numarası gibi göründüğünü biliyorum ama değil. WASM'ye derlenen ve Origin Özel Dosya Sisteminde (OPFS) kalıcı hale getirilen SQLite, tarayıcıda size gerçek bir ilişkisel veritabanı sunar. Tam SQL sorguları. İşlemler. İndeksler. Eserler.

OPFS, bunu pratik hale getiren daha yeni API'dir. Web uygulamalarına, SQLite'ın tam olarak ihtiyaç duyduğu şey olan, yüksek performanslı eşzamanlı erişime sahip (Web Çalışanlarında) korumalı alanlı bir dosya sistemi sağlar. OPFS'den önce, SQLite'ı bellekte çalıştırabilir ve işe yarayan ancak yavaş ve kırılgan olan IndexedDB'ye manuel olarak devam edebiliyordunuz.

Gerçek bir projede başlatmanın kabaca nasıl göründüğü aşağıda açıklanmıştır (burada en iyi şansı yakaladığım kütüphane olan wa-sqlite kullanıyorum):

'wa-sqlite'dan { SQLiteAPI }'yi içe aktarın; 'wa-sqlite/src/examples/OPFSCoopSyncVFS.js'den { OPFSCoopSyncVFS }'yi içe aktarın; eşzamansız işlev initDatabase() { const module = wait SQLiteAPI.initialize(); const vfs = new OPFSCoopSyncVFS('pm-tool-db'); wait vfs.initialize(module); const db = wait module.open_v2('workspace.db'); // HACK: wa-sqlite, Safari'de eşzamanlı yazma işlemlerini iyi bir şekilde gerçekleştiremez, // bu nedenle bir kuyruk üzerinden serileştirme yapıyoruz. Bkz. vlcn-io/wa-sqlite#247 wait module.exec(db, `PRAGMA Journal_mode=WAL`); wait module.exec(db, ` CREATE TABLE IF NOT EXISTS görevler ( kimlik METİN BİRİNCİL ANAHTARI, başlık METİN BOŞ DEĞİL, durum METİN VARSAYILAN 'biriktirme', atanan_id METİN, proje_id METİN NULL DEĞİL, konum GERÇEK VARSAYILAN 0, created_at METİN VARSAYILAN (tarihsaat('şimdi')) güncellendi_at METİN VARSAYILAN (tarihsaat('şimdi')) ) `); db'yi döndür; }

Üretimde, tüm veritabanı erişimini mutasyonları serileştiren bir yazma kuyruğuna sarıyorum. Ayrıca, Sentry'ye yapılan her başarısız yazma işlemini tam SQL ifadesiyle (tabii ki PII'den arındırılmış) günlüğe kaydediyorum çünkü bir kullanıcının tarayıcısındaki veritabanı sorunlarının hatalarını ayıklamak, bu telemetri olmadan cehennemdir.

Neredeyse iki günümü boşa harcadığım bir sonuç: Safari'nin OPFS uygulaması, Chrome'unkinden incelikli şekillerde farklı davranıyor. Spesifik olarak, Safari 18'deki belirli iframe bağlamlarında createSyncAccessHandle() işlevinin sessizce başarısız olmasına neden olan bir hatayla karşılaştım. Hata yok, istisna yok. Sadece işe yaramıyor. Daha yavaş ama en azından işleyen Safari'de IndexedDB destekli kalıcılığa geri döndüm. (Safari 19⁄ 26'nın bunu düzelttiği söylendi, ancak henüz doğrulamadım.)

Gerçekte kullandığım seçeneklerin hızlı karşılaştırması:

Depolama İyidir IndexedDB'ye Dikkat Edin Geniş uyumluluk, orta düzeyde veri Kötü DX, SQL yok, ayrıntılı OPFS + SQLite WASM İlişkisel veriler, karmaşık sorgular, ciddi uygulamalar Safari tuhaflıkları, ~400 KB paket ekleme PGlite (WASM'de Postgres) İstemcide tam Postgres uyumluluğu Daha yeni, daha büyük paket, hâlâ olgunlaşıyor

Ayrıca CRDT sütun desteğini doğrudan SQLite tablolarına ekleyen cr-sqlite'ı da denedim. Zekice bir fikir, ancak 2025'in sonlarında değerlendirdiğimde bunu üretimde kullanım için çok erken bir aşamada buldum. Birleştirme anlambilimi bazen şaşırtıcıydı ve SQLite içindeki CRDT durumunda hata ayıklamak acı vericiydi. Bu yılın ilerleyen zamanlarında tekrar ziyaret edeceğim.

Aslında Zor Olan Kısım

Verilerin yerel olarak depolanması çözülmüş bir sorundur. Bunu cihazlar ve kullanıcılar arasında güvenilir bir şekilde senkronize etmek, beyaz saçlarınızı kazandığınız yerdir.

Birden çok kopya bağımsız olarak okuyup yazabildiğinde, değişiklikleri uzlaştıracak bir mekanizmaya ihtiyacınız olur. Temel olarak dört yaklaşım var ve ben bunlardan üçünü kullandım.

CRDT'ler (Çakışmasız Çoğaltılmış Veri Türleri), eşzamanlı düzenlemelerin her zaman çakışma olmadan birleştirilebilmesi için tasarlanmış, matematiksel olarak garanti edilmiş veri yapılarıdır. Yjs, JavaScript'teki en popüler uygulamadır ve gerçek zamanlı, işbirliğine dayalı metin düzenleme için gerçekten mükemmeldir. Bunu son şirketimde işbirliğine dayalı bir belge düzenleyici oluşturmak için kullandım ve deneyim çoğunlukla iyiydi, ancak çatışma çözümü bölümünde sıkıntılı noktalara değineceğim.

Paylaşılan bir Yjs belgesinin ayarlanması pratikte şu şekilde görünür:

* 'yjs'den Y olarak içe aktarın; 'y-websocket'ten { WebsocketProvider }'ı içe aktarın; const ydoc = new Y.Doc(); const sağlayıcı = new WebsocketProvider( 'wss://sync.our-app.dev', 'workspace-a1b2c3d4', ydoc ); const görevler = ydoc.getMap('görevler'); // Bir görev ekle const görev = new Y.Map(); Task.set('title', '3. Çeyrek yol haritası taslağını inceleyin'); görev.set('tamamlandı', yanlış); görev.set('atanan', 'maria'); // TODO: bunu bir kez düzgün bir şekilde yazın; yjs, iç içe haritalar için daha iyi TS türlerini // dışa aktarır. Şimdilik bu iyi çalışıyor. görevler.set('f47ac10b-58cc-4372-a567-0e02b2c3d479', herhangi bir görev); görevler.observeDeep(() => { // Kullanıcı arayüzünü yeniden oluştur. Pratikte geri dönüyorum bu ~16 ms'ye kadar // çünkü gözlemDeep aktif işbirliği sırasında bir LOT başlatır renderTaskList(tasks.toJSON()); });

Automerge, Rust tarafından desteklenen ve belge odaklı bir modele sahip diğer büyük CRDT kütüphanesidir. Daha az kullandım ama buna yemin eden takımlar biliyorum. Loro daha yeni, Rust tabanlı ve daha iyi performans iddia ediyor. Henüz Loro ile hiçbir şey göndermedim.

Veritabanı çoğaltma diğer büyük yaklaşımdır ve dürüst olmak gerekirse, Google Dokümanlar tarzı gerçek zamanlı metin düzenlemeye ihtiyaç duymayan çoğu uygulama için bunun daha iyi bir seçim olduğunu düşünüyorum. Fikir oldukça basit: tesisatı yöneten bir senkronizasyon motoruyla bir sunucu veritabanı (Postgres) ile bir istemci veritabanı (SQLite) arasındaki satırları çoğaltın.

PowerSync bunu iyi yapıyor. Mutasyonlar için geri yazma yolu ile Postgres'ten istemci SQLite'a tek yönlü kopyalama sağlar. ElectricSQL daha iddialı; Postgres ve SQLite arasında tam aktif-aktif senkronizasyona gidiyor. Üretimde PowerSync'i, prototiplerde ise ElectricSQL'i kullandım. PowerSync, her ikisini de 2026'nın başlarında değerlendirdiğimde daha kararlı hissettim, ancak ElectricSQL'in yaklaşımı, yürütmeyi başarırsa daha güçlü olur.

Triplit tamamen farklı bir bakış açısına sahip: yerleşik senkronizasyona sahip tam yığınlı bir veritabanı olduğundan, "istemci veritabanı" ve "sunucu veritabanı"nı ayrı ayrı düşünmezsiniz. Hafta sonu prototipi dışında denemedim ancak geliştirici deneyimi şaşırtıcı derecede güzeldi.

Olay kaynağı oluşturma (geçerli durum yerine mutasyon günlüğünü senkronize etme) LiveStore'un benimsediği yaklaşımdır. Bunu entelektüel açıdan çekici ve ara sıra yararlı buluyorum, ancak pratikte, durumu bir olay günlüğünden yeniden yapılandırmanın çoğu uygulamanın ihtiyaç duymadığı karmaşıklığı artırdığını gördüm. Tartışmalı görüşüm: Uygulama geliştirme için olay kaynağı kullanımı fazlasıyla tavsiye ediliyor. Denetim günlükleri ve belirli alanlar için harikadır, ancak görev panosu için mi? Sadece satırları senkronize edin.

Herkes buna katılmayacaktır. Etkinlik kaynağının tutkulu savunucuları olduğunu biliyorum ve konferanslarda bu konuda en az iki kez yanıldığım söylendi. Belki de henüz bunun için doğru uygulamayı geliştirmedim.

Çatışmalar: Herkesin Korktuğu Şey

Çatışma çözümünün korkunç, çözülemez bir sorun olduğunu düşünürdüm. Bu sorunu çözebilecek üç uygulama geliştirdikten sonra bunu şu şekilde revize edeceğim: Bu, özel veri modeliniz hakkında dikkatli bir şekilde düşünmenizi gerektiren yönetilebilir bir sorundur ve çoğu geliştirici bu konuda gereğinden fazla düşünür.

İki kopya birbirinin değişikliklerini görmeden aynı verileri değiştirdiğinde çakışmalar meydana gelir. A Kullanıcısı, çevrimdışıyken telefonunda bir görev başlığını düzenler. B Kullanıcısı aynı başlığı dizüstü bilgisayarında düzenler. İkisi de tekrar çevrimiçi oluyor. Şimdi ne olacak?

Bunu ele almaya yönelik ilk girişimim utanç verici derecede saftı:

// İlk denemem. Bunu yapma. functionsololConflict(local: herhangi biri, uzak: herhangi biri) { // sadece... uzaktaki olanı al? Elbette? uzaktan dönüş; }

Sorun çok açık: yerel değişiklikler sessizce iptal ediliyor. A Kullanıcısı bir başlığı düzenler, senkronize eder ve düzenlemeleri kaybolur. Bunun olduğunu bile bilmiyorlar.

Aslında çoğu durumda işe yarayan şey, kayıt düzeyinde değil, saha düzeyinde son yazma-kazançlarıdır (LWW). A Kullanıcısı başlığı değiştirirse ve B Kullanıcısı son tarihi değiştirirse, farklı alanlara dokundukları için her iki değişikliği de korursunuz. Yalnızca her ikisi de aynı alanı değiştirdiğinde gerçek bir çelişki yaşarsınız ve ardından daha sonraki zaman damgasını seçersiniz.

arayüz FieldValue { değer: string | sayı | boole; // Çoğu bağı koparmak için yeterli hassasiyete sahip ISO zaman damgası güncellendiAt: string; // Zaman damgaları eşleştiğinde eşitliği bozan istemci kimliği. // Bu düşündüğünüzden daha sık oluyor. clientId: dize; } function pickWinner(a: FieldValue, b: FieldValue): FieldValue { const timeA = new Date(a.updatedAt).getTime(); const timeB = new Date(b.updatedAt).getTime(); if (zamanA !== zamanB) dönüş zamanA > zamanB ? a : b; // Zaman damgaları eşleştiğinde deterministik eşitlik bozucu return a.clientId > b.clientId ? a : b; } // Uygulamada, bunu alan bazında tüm kayıt boyunca uyguluyorum. function mergeTask(local: Record, uzak: Record) { const merged: Record = {}; const allKeys = new Set([...Object.keys(local), ...Object.keys(remote)]); for (allKeys'in const anahtarı) { if (!local[anahtar]) { birleştirilmiş[anahtar] = uzak[anahtar]; devam etmek; } if (!uzak[anahtar]) { birleştirilmiş[anahtar] = yerel[anahtar]; devam etmek; } birleştirilmiş[anahtar] = pickWinner(yerel[anahtar], uzak[anahtar]); } dönüş birleştirildi; }

Üretim uygulamamızda bu, çakışmaların yaklaşık %95'ini kullanıcının görebileceği herhangi bir sorun olmadan çözer. Geri kalan durumlarda (aynı metin alanını düzenleyen iki kişi), LWW, bir kişinin sessizce düzenlemesinin kazanacağı anlamına gelir. İçin görev başlığı mı? Dürüst olmak gerekirse, bu genellikle iyidir. Bir belge gövdesi için mi? Hayır. Burası CRDT'lerin geçimini sağladığı yer.

Ancak, onunla karşılaşana kadar farkına varmadığım daha incelikli bir sorun var: anlamsal çatışmalar. Veriler yapısal düzeyde temiz bir şekilde birleşiyor, ancak sonuç anlamsız. Her ikisi de çevrimdışı olan iki kullanıcı, farklı toplantılarla aynı saat 14.00 toplantı aralığını rezerve ediyor. Alan düzeyinde birleştirme, farklı kayıtlara yazdıkları için her iki yazma işlemini de kabul eder. Yapısal bir çatışma yok. Ancak çifte rezervasyonunuz var ve birleştirme fonksiyonunuz bunun bir sorun olduğunu bilmiyor.

Anlamsal çakışmalar uygulama düzeyinde doğrulama gerektirir ve bunun senkronizasyon sırasında sunucuda gerçekleşmesi gerekir. Senkronizasyon motorunuz verileri yapısal olarak birleştirir, ancak sunucunuzun sonucu kabul etmeden önce etki alanı değişmezlerini kontrol etmesi gerekir. Benim ulaştığım yaklaşım (iki kez yanlış yaptıktan sonra): geri yazma aşamasında sunucuda doğrulama yapın, ancak ihlalleri sessizce reddetmek yerine işaretleyin.

Demek istediğim şu. İstemci senkronizasyon sırasında mutasyonları sunucuya gönderdiğinde, sunucu bunları Postgres'e uygulamadan önce bir kısıtlama doğrulama katmanı üzerinden çalıştırır:

arayüz SyncViolation { type: 'scheduling_conflict' | 'kapasite_aşıldı' | 'eski_atama'; kayıt kimliği: dize; açıklama: dize; // İstemcinin bağlamı gösterebilmesi için çakışan kayıtlar çakışan Kayıtlar: string[]; // Bu ihlal ne zaman tespit edildi? At: string; } eşzamansız işlev validateSyncBatch( mutasyonlar: SyncMutation[], serverDb: Database ): Promise<{ kabul edildi: SyncMutation[]; ihlaller: SyncViolation[] }> { const kabul edildi: SyncMutation[] = []; const ihlalleri: SyncViolation[] = []; for (mutasyonların sabit mutasyonu) { if (mutation.table === 'calendar_events') { // Çift rezervasyon const örtüşmesini kontrol edin = wait serverDb.query( `SELECT id, title FROM takvim_events WHERE oda_id = ? AND id != ? AND start_time < ? AND end_time > ?`, [mutation.data.room_id, mutasyon.data.id, mutasyon.veri.bitiş_zamanı, mutasyon.veri.başlangıç_zamanı] ); if (overlapping.length > 0) { ihlaller.push({ type: 'scheduling_conflict', kayıtId: mutasyon.data.id, açıklama: `"${overlapping[0].title}" ile çakışıyor`, çakışanRecords: örtüşen.map(r => r.id), tespit edilenAt: new Date().toISOString() }); // Yazmayı hala kabul ediyorum, ancak işaretliyorum // Alternatif olarak onu reddediyorum, ancak bu durumda kullanıcının // yerel durumu ile sunucu durumu birbirinden ayrılıyor ve bu daha da kötü kabul ediliyor.push(mutation); devam etmek; } } kabul edildi.push(mutasyon); } return { kabul edildi, ihlaller }; }

Buradaki temel karar - ve bu konuda ileri geri gittim - çelişkili yazıyı kabul etmemiz ve onu tamamen reddetmek yerine işaretlememizdir. Reddederseniz, kullanıcının yerel veritabanında sunucunun kabul etmeyi reddettiği bir kayıt bulunur ve artık kurtarılması gerçekten zor olan bir durum farklılığı durumundasınızdır. İlk önce reddetme yaklaşımını denedim ve bu, istemcide, sunucuda bulunmadığı için kullanıcıların silemediği hayalet kayıtlara yol açtı. Kabus.

Bunun yerine, sunucu yazmayı kabul eder, ihlali saklar ve ihlali istemciye geri senkronize eder. Müşteri engellemeyen bir bildirim gösteriyor: "'3. Çeyrek Planlama' toplantınız saat 14.00'te B Odasındaki 'Tasarım İncelemesi' ile çakışıyor. Çözümlemek için dokunun." Kullanıcı dokunur, her iki toplantıyı da görür ve yeniden planlamak veya iptal etmek için birini seçer. Çözünürlük, geri senkronize edilen normal bir yazmadır.

Bu mükemmel mi? Hayır. İhlalin oluşturulduğu zaman ile kullanıcının sorunu çözdüğü zaman arasında, her iki çakışan kaydın da bulunduğu bir pencere vardır. Toplantı odaları için bu kabul edilebilir bir durumdur. İki kişinin son öğeyi "satın aldığı" envanter yönetimi gibi bir şey için bu pencere kabul edilemez ve tam da bu yüzden daha önce güçlü işlem tutarlılığı gerektiren sistemler için önce yerelin yanlış olduğunu söylemiştim.

Hala bu modeli yineliyorum. Kullanıcılar bildirimleri dikkate almazsa ihlal tablosu büyür (72 saat sonra bildirimlerin süresi dolar, bu da keyfi bir durum gibi görünür). Sunucuda hangi değişmezlerin doğrulanacağına karar vermek, aslında istemci tarafı uygulama mantığınızın dışında paralel bir iş kuralları kümesini korumanızı gerektirir. Zarif değil. Ancak işe yarıyor ve geliştirdiğim uygulama sınıfı için bulduğum en iyi yaklaşım bu. Daha temiz bir şey yaptıysanız bunu gerçekten duymak isterim.

Yjs gibi CRDT'ler için karakter düzeyinde (metin için) çakışma çözümü son derece iyi çalışıyor. Aynı paragrafı yazan iki kişi, her iki karakter kümesinin de anlamlı bir sırada göründüğünü görecektir. Ancak yapılandırılmış verilerin CRDT ile birleştirilmesi (haritalar, diziler, iç içe geçmiş nesneler) sizi şaşırtacak sonuçlar üretebilir. Bir keresinde Yjs destekli bir görev listesinin birleştirme sonrasında öğelerin yinelendiğini izlemiştim çünkü iki kullanıcı aynı listeyi çevrimdışı olarak yeniden sıralamıştı ve CRDT'nin liste birleştirme semantiği sıralamalarını araya eklemişti. Teknik olarak doğru. Pratik olarak kafa karıştırıcı. Birleştirme sonrası tekilleştirme adımını ekledik; bu bir hack gibi geldi ama sorunu çözdü.

Çakışmaları ne zaman kullanıcıya Git tarzında sunmalısınız? Deneyimlerime göre, tipik uygulama verileri için neredeyse hiçbir zaman. Kullanıcılar birleştirme çakışmalarını çözmek istemiyor. Uygulamanın bunu çözmesini istiyorlar. Bunun istisnası yüksek riskli içeriklerdir: yasal belgeler, tıbbi kayıtlar, bir düzenlemeyi sessizce bırakmanın gerçek zarara yol açabileceği her şey.

Size 2026 ortası itibarıyla mevcut olan araçlarla ilgili dürüst okumamı sunacağım, ancak bu alanın yeterince hızlı hareket ettiği ve bazılarının siz okuduğunuzda güncelliğini yitirmiş olabileceği uyarısında bulunacağım.

Yjs en olgun CRDT kütüphanesidir. Üretime hazır, büyük topluluk, çoğu işbirliğine dayalı editörle (TipTap, BlockNote, Lexical) entegre olur. Gerçek zamanlı işbirliğine dayalı düzenlemeye ihtiyacınız varsa buradan başlayın.

Automerge sağlamdır, Rust desteklidir ve Yjs'den daha belge odaklı bir yaklaşım benimser. Veri modelinin bir belge metaforuna uyduğu uygulamalarda bunun iyi kullanıldığını gördüm. Yjs'den daha az entegrasyon var ancak çekirdek iyi tasarlanmış.

PowerSync, mevcut bir Postgres arka ucuna sahip olan ve çevrimdışı destek eklemek isteyen ekiplere önerdiğim şeydir. Üretime hazırdır, dokümanlar iyidir ve zihinsel modelin (Postgres istemci SQLite ile senkronize edilir, istemci yazmaları tanımlanmış bir yükleme yolundan geçer) akıl yürütmesi kolaydır. Uygulamamızda, yaklaşık 5.000 görev içeren bir çalışma alanı için ilk senkronizasyon, düzgün bir bağlantıda yaklaşık 1,2 saniye ve kısıtlanmış bir 3G simülasyonunda yaklaşık 3,5 saniye sürer. Bu bizim için kabul edilebilirdi.

ElectricSQL daha iddialı bir şeye doğru gidiyor: Postgres ve SQLite arasında gerçek aktif-aktif replikasyon, hangi verinin hangi istemciyle senkronize edileceğini "şekiller" tanımlıyor. Bunun başarılı olmasını istiyorum çünkü prototiplerdeki geliştirici deneyimi mükemmeldi. Ancak Şubat 2026'da üretim için değerlendirdiğimde yeterince pürüzlerle karşılaştım (özellikle şekil yönetimi ve yeniden bağlanma davranışı konusunda) ve bunun yerine PowerSync'i tercih ettim. Tekrar ziyaret etmeyi planlıyorum.

Triplit bir hafta sonu prototipinde beni etkiledi. Yerleşik senkronizasyona sahip tam yığın veritabanı, güzel TypeScript API. Gerçek üretim yüküyle stres testi yapmadım ve taahhütte bulunmadan önce bunu yapmak isterdim.

Sıfır (Rocicorp'tan Replicache çalışanları) ilginçtir çünkü senkronizasyon için satır çoğaltma modelinden farklı olan sorgu tabanlı bir yaklaşım gerektirir. Replicache, Zero lehine gün batımıydı, bu da size bu alanda yaklaşımların ne kadar hızlı geliştiğine dair bir şeyler anlatıyor. İzlemeye değer, ancak henüz bir üretim uygulaması için bunun üzerine geliştirme yapmayacağım.

TinyBase, daha küçük uygulamalar veya prototip oluşturma için mükemmel olan hafif bir reaktif mağazadır. Bunu kişisel bir yan proje (okuma izleyici) için kullandım ve çok beğendim. Ekip ölçeğinde bir ürün için kullanacağımdan emin değilim.

PGlite (WASM'ye derlenen Postgres) çılgındır. İstemci ve sunucuda aynı SQL lehçesi. ElectricSQL ile birleştirildiğinde teorik olarak aynı sorguları her yerde çalıştırabilirsiniz. Uzun vadede işlerin bu yönde ilerlediğini düşünüyorum ancak PGlite'ın paket boyutu ve bellek alanı mobil tarayıcılar için hala endişe kaynağı.

Replicache gün batımının bana öğrettiği bir şey var: mimarinizi, yedek planı olmayan küçük bir şirketin tek bir aracına güvenmeyin. Senkronizasyon katmanımı, motorları aylar yerine birkaç hafta içinde değiştirebilecek kadar soyut tutuyorum. Bunun erken bir soyutlama gibi göründüğünü biliyorum ama bu kadar genç bir alanda bunun sadece sağduyulu bir yaklaşım olduğunu düşünüyorum.

Gerçek Bir Uygulama Oluşturma: Mimari, Kimlik Doğrulama ve Geçişler

Pratikte yerel öncelikli bir uygulamayı nasıl yapılandırdığımı anlatmak istiyorum çünkü blog yazılarında gördüğünüz katman diyagramları kodun neye benzediğiyle nadiren eşleşiyor.

İşbirliğine dayalı bir proje yönetimi aracı için mevcut yığınım şöyle görünüyor:

Kullanıcı Arayüzü: Veri okumaları için hiçbir zaman fetch() işlevini çağırmayan bileşenlere tepki verin.

Hiçbir zaman veri okuması gerektirmeyen bileşenlere tepki verin. Sorgu katmanı: Yerel SQLite veritabanına abone olan ve veriler değiştiğinde otomatik olarak yeniden oluşturulan LiveQuery kancalarını kullanın.

Yerel SQLite veritabanına abone olan ve veriler değiştiğinde otomatik olarak yeniden oluşturulan kancalar. Yerel veritabanı: wa-sqlite aracılığıyla SQLite, OPFS'de kalıcı oldu.

Wa-sqlite aracılığıyla SQLite, OPFS'ye devam etti. Mutasyon katmanı: Yerel SQLite'a karşı düz INSERT / UPDATE / DELETE ifadeleri. Yerel SQLite'a karşı düz / / ifadeleri. Senkronizasyon: PowerSync, yerel SQLite ile Postgres arka ucumuz arasındaki çoğaltmayı yönetir.

PowerSync, yerel SQLite ile Postgres arka ucumuz arasında çoğaltmayı yönetir. Sunucu: Postgres, bir Node.js kimlik doğrulama hizmeti ve küçük bir senkronizasyon doğrulama katmanı.

Bileşen kodu, eskiden yazdıklarımla karşılaştırıldığında neredeyse saçma sapan derecede basit görünüyor:

{ useLiveQuery }'yi '@powersync/react' adresinden içe aktarın; '../lib/database'den { db }'yi içe aktarın; function TaskBoard({ projeId }: { projeId: string }) { const görevler = useLiveQuery( `SELECT * FROM görevler NEREDE proje_id = ? VE arşivlenmiş = 0 ORDER BY konum`, [projeKimliği]); async function addTask(title: string) { wait db.execute( `INSERT INTO görevler (id, başlık, proje_id, konum, created_at) VALUES (?, ?, ?, ?, datetime('now'))`, [crypto.randomUUID(), başlık, projeId, görevler.uzunluk] ); // İşte bu kadar. useLiveQuery, değişikliği otomatik olarak alır. // Geçersiz kılma yok, yeniden getirme yok, yükleme durumu yok. } // Yükleniyor kontrolü yok. Veriler yereldir. İlk senkronizasyondan sonra her zaman oradadır. return (

{tasks.map(task => )}
); }

Bunu, kodun en az iki katı olan ve yükleme durumlarını, hata durumlarını, geri alma ile iyimser güncelleme mantığını ve önbellek geçersiz kılmayı içeren React Query + REST eşdeğeriyle karşılaştırın. Kaçırmıyorum.

Yerel Birinci Dünyada Kimlik Doğrulama

Kimlik doğrulama, geleneksel uygulamalarla hemen hemen aynı şekilde çalışır: JWT belirteçleri, OAuth akışları ve oturum yönetimi. Belirteç, her bir isteğin yerine senkronizasyon bağlantısının kimliğini doğrular. Çevrimdışı erişim, veriler zaten yerel olduğundan çalışır. Veriler orijinal olarak senkronize edildiğinde kullanıcının kimliği doğrulandı.

Yetkilendirme daha yanıltıcıdır ve bence yerel öncelikli makalelerin çoğu bunu yeterince açıklamıyor. Veritabanınızın tamamını her istemciyle senkronize edemezsiniz ve yetkisiz verileri gizlemek için istemci tarafı koduna güvenemezsiniz. Birisi DevTools'u açacak, yerel SQLite dosyasını bulacak ve her şeyi görecek. Müşteri bir güven sınırı değildir.

Senkronizasyon katmanında yetkilendirmeyi zorunlu kılarsınız. PowerSync'te hangi satırların hangi istemcilere gideceğini tanımlayan "senkronizasyon kuralları" bulunur. ElectricSQL'in "şekilleri" vardır. Her iki durumda da sunucu yalnızca kullanıcının görmeye yetkili olduğu verileri gönderir. İstemci geri yazma gönderdiğinde, sunucu bunları Postgres'e uygulamadan önce yetkilendirme kurallarına göre doğrular. Kullanıcı yapmaması gereken bir şeyi değiştirmeye çalışırsa sunucu senkronizasyon sırasında bunu reddeder.

Ayrıca uçtan uca şifrelemeden (E2EE) de bahsetmek istiyorum çünkü doğal olarak önce yerel ile eşleşiyor. Veriler istemcide bulunduğundan, senkronizasyondan önce bunları şifreleyebilirsiniz. Sunucu, okuyamadığı şifreli blobları saklar ve iletir. Anytype gibi uygulamalar bunu yapar. Mevcut uygulamamızda E2EE'yi uygulamadık ancak daha hassas verileri ele aldığımızda yol haritamızda yer alıyor.

Binlerce Cihazda Şema Taşıma İşlemleri

Bu beni ilk defa hazırlıksız yakaladı. Sunucuda, kontrol ettiğiniz bir veritabanına karşı geçiş işlemi gerçekleştirirsiniz. İstemcide her kullanıcının, uygulamayı en son ne zaman açtığına bağlı olarak şemanızın herhangi bir sürümünü çalıştırabilecek kendi veritabanı vardır.

Uygulama başlangıcında sürüm numarasını kontrol eden basit bir geçiş çalıştırıcısı kullanıyorum:

const MIGRATIONS = [ { version: 1, sql: ` CREATE TABLE IF NOT EXISTS görevler ( id TEXT PRIMARY KEY, title TEXT NOT NULL, status TEXT DEFAULT 'backlog', project_id TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now')) ); ` }, { version: 2, // Sprint 4 sql'ye öncelik ve son tarih eklendi: ` ALTER TABLE görevleri ADD COLUMN önceliği INTEGER DEFAULT 0; ALTER TABLE görevleri ADD COLUMN son tarih METİNİ; ` }, { version: 3, // Çevrimdışı görüntüleme için normalleştirilmemiş atanan adı. // Evet, bunun bir takas olduğunu biliyorum. JOIN, düşük seviye Android cihazlarda // performansı öldürüyordu. sql: ` ALTER TABLE görevleri ADD COLUMN atanan_adı METİN VARSAYILANI ''; ` } ]; async function runMigrations(db: Database) { wait db.execute(` CREATE TABLE IF NOT EXISTS _schema_version (versiyon INTEGER) `); const satırları = wait db.execute('_schema_version'dan sürümü SEÇİN'); const currentVersion = satırlar.uzunluk > 0 ? satırlar[0].versiyon : 0; for (MIGRATIONS'ın const geçişi) { if (migration.version > currentVersion) { console.log(`Yerel veritabanının v${migration.version}'a taşınması`); wait db.execute('BEGIN'); try { wait db.execute(migration.sql); wait db.execute( '_schema_version (satır kimliği, sürüm) DEĞERLERE EKLEYİN VEYA DEĞİŞTİRİN (1, ?)', [migration.version] ); wait db.execute('COMMIT'); } catch (hata) { wait db.execute('ROLLBACK'); // Üretimde bu, // geçiş sürümüyle birlikte bir Sentry uyarısı tetikler ve hata ayrıntıları throw err; } } } }

Geçişlerinizi katkı sağlayacak şekilde tasarlayın. Varsayılanlara sahip yeni sütunlar. Yeni tablolar. Kesinlikle gerekmedikçe sütunları yeniden adlandırmayın veya bırakmayın, çünkü eski uygulama sürümlerini çalıştıran kullanıcılar verileri senkronize etmeye devam edecek ve sunucunuzun uyumsuzluğu gidermesi gerekiyor. Bunu, eski bir müşterinin hâlâ yazmakta olduğu bir sütunu bıraktığımda zor yoldan öğrendim; bu, bir hafta sonu boyunca yaklaşık 200 kullanıcı için sessiz senkronizasyon hatalarına neden oldu. Eğlenceli değil.

Bugün Yeni Bir Projeye Başlasaydım

Bu bana çok soruluyor, işte şu anki cevabım. Her altı ayda bir değişir.

Gerçek zamanlı özelliklere ve çevrimdışı desteğe sahip ortak çalışmaya dayalı bir uygulama için şununla başlayacağım: Ön uçta React, senkronizasyon için PowerSync, istemcide wa-sqlite aracılığıyla SQLite (Safari için IndexedDB geri dönüşüyle ​​OPFS'de ısrar edildi) ve Supabase (bu da bana Postgres, kimlik doğrulama ve satır düzeyinde güvenlik sağlıyor). Yjs'yi yalnızca zengin metin işbirliğine ihtiyacım olursa kullanırdım ve ihtiyaç duymasaydım bundan kaçınırdım çünkü CRDT'ler veri modelinize anlamlı bir karmaşıklık katar.

Çoğunlukla çevrimdışı desteğe ve anında okumaya ihtiyaç duyduğum ancak işbirliğinin ikincil olduğu daha basit bir uygulama için senkronizasyon motorunu tamamen atlayabilir ve yalnızca bir REST API'sinden iten/çeken özel bir senkronizasyon katmanına sahip yerel bir SQLite veritabanı kullanabilirim. Bunun tekerleği yeniden icat etmek gibi göründüğünü biliyorum, ancak basit durumlarda, tamamen anladığınız özel bir senkronizasyon, ihtiyacınız olmayan kavramları ekleyen genel amaçlı bir senkronizasyon motorundan daha iyidir.

Şu anda üretim için ElectricSQL veya Zero'yu kullanmayacağım, kötü oldukları için değil, ancak görüşme yaptığım bir şey için onlara güvenmeden önce 6-12 ay daha olgunluk istediğim için. Daha önce erken aşamadaki altyapıyı inşa ettiğim için yanmıştım (bu size bir şey anlatırsa Meteor'u ilk benimseyenlerden biriydim) ve şimdi yenilik riskini nerede kabul edeceğim konusunda daha temkinliyim.

Performans: Aslında Hızlı Olan ve Acı Veren Nedir?

Okumalar anında gerçekleşir. Bu pazarlama değil. 500 görevden oluşan bir liste için yerel bir SQLite veritabanını sorgulamak, M2 MacBook'umda iki milisaniyenin altında ve orta sınıf bir Android telefonda yaklaşık sekiz milisaniye sürüyor. Ağ yok. Döndürücü yok. Yükleme durumu yok.

Yazmalar da anında gerçekleşir. INSERT INTO görevleri yerel olarak çalışır, kullanıcı arayüzü tepkisel olarak güncellenir ve senkronizasyon her zaman gerçekleşir. Kullanıcılar yazma işlemlerini anlık olarak algılarlar çünkü öyledirler.

İlk senkronizasyon, maliyeti ödediğiniz yerdir. Yerel kopyanın ilk yüklemede (veya yeni bir cihazda) önyüklenmesi, potansiyel olarak megabaytlarca verinin indirilmesi anlamına gelir. Uygulamamızda 5.000 görev, 200 proje ve 50 kullanıcıdan oluşan bir çalışma alanı, geniş bantta yaklaşık 1,2 saniye, yavaş mobil bağlantıda ise dört ila beş saniye sürer. Kısmi senkronizasyonla (yalnızca kullanıcının aktif projelerini senkronize ederek) ve ilk senkronizasyon sırasında tek seferlik "Çalışma alanınızı ayarlama" ekranını göstererek bu durumu hafifletiyoruz. Bu ilk senkronizasyondan sonra artımlı güncellemeler çok küçüktür.

Paket boyutu gerçek bir endişe kaynağıdır. WASM'ye derlenen SQLite, JavaScript paketinize yaklaşık 400 KB gzip'li dosya ekler. Bu önemsiz bir şey değil, özellikle de mobil cihazlarda Time to Interactive'i önemsiyorsanız. Veritabanı modülünü dinamik içe aktarma() ile yavaş yüklüyorum, böylece ilk oluşturmayı engellemez.

Bellek diğer bir sorundur. SQLite WASM bellekte çalışır ve agresif bellek sınırlarına sahip mobil tarayıcılarda büyük bir veritabanı sekme çökmelerine neden olabilir. Kısmi senkronizasyon yoluyla senkronize edilmiş veri kümesini küçük tutmak ve eski verileri budama konusunda agresif davranmak dışında bunun için harika bir çözüm bulamadım.

Not: Bellek sorunlarından bahsetmişken, Martin Kleppmann'ın Veri Yoğun Uygulamalar Tasarlama kitabını üçüncü kez okuyorum. Her tekrar okuduğumda yeni bir şeyler yakalıyorum. Okumadıysanız ve dağıtılmış verileri düşünüyorsanız, önce durun ve okuyun.

Bunu kısa tutacağım çünkü dürüst cevap, yerel öncelikli uygulamaları test etmenin geleneksel uygulamaları test etmekten daha zor olduğu ve araçların henüz mükemmel olmadığıdır.

Benim için işe yarayan şey: birleştirme mantığı için birim testleri (bunlar saf işlevlerdir, test edilmesi kolaydır), bellekteki iki istemci örneğini döndüren ve eşzamanlı düzenlemelerden sonra bunların birleştiğini doğrulayan entegrasyon testleri ve çevrimdışı/çevrimiçi geçişleri simüle etmek için context.setOffline(true) kullanan Playwright E2E testleri.

İyi çözemediğim şey: yalnızca çatışma çözümü sırasında meydana gelen hataların belirli bir zamanlamayla yeniden üretilmesi. Bir kullanıcı bir görevin "açıklamasını kaybettiğini" bildirdiğinde, çoğu zaman onu yeniden oluşturamıyorum çünkü tam olarak hangi çevrimdışı düzenleme sırasını ve ne şekilde yapılacağını bilmiyorum. senkronizasyon olayları çakışmaya yol açtı. Senkronizasyon olaylarını daha ayrıntılı bir şekilde kaydetmeye (neler gönderildi, ne alındı, hangi çakışmalar tespit edildi, nasıl çözüldü) ve bu günlükleri gözlemlenebilirlik yığınımıza göndermeye başladım. Yardımcı oluyor ama istediğim kadar temiz değil.

Hızlı kontrol gibi bir şeyle özellik tabanlı testler, CRDT mantığı için gerçekten faydalıdır. Rastgele işlem dizileri oluşturun, bunları rastgele sıralarda uygulayın ve yakınsamayı iddia edin. Keşke bunu daha önce yapmaya başlasaydım.

İzlediklerim Beni Endişelendiriyor

Bunun nereye varacağı konusunda heyecanlıyım. PGlite (tarayıcıdaki tam Postgres), istemci/sunucu veri katmanı ayrımının ortadan kalktığı bir geleceğe kısa bir bakış gibi geliyor. SQL yazarsınız, her yerde çalışır, senkronizasyon mimari bir karardan ziyade çalışma zamanı meselesidir. Henüz orada değiliz ama buradan görebilirsiniz.

Ayrıca yerel öncelik ile yapay zekanın yakınsamasını da izliyorum. Modelleri yerel olarak çalıştırma, verileri cihazda tutma, bulut yapay zekasını yalnızca açık izinle ve şifrelenmiş verilerle kullanma. Gizlilikle ilgili sonuçlar ilgi uyandırıcı ve yapay zekanın yazılım deneyiminden daha fazla yararlanması nedeniyle "verileriniz asla cihazınızdan ayrılmayacak" ifadesinin gerçek bir ürün farklılaştırıcısı haline geleceğini düşünüyorum.

Beni endişelendiren şey parçalanma. Her senkronizasyon motoru kendi protokolünü kullanır. Hiçbir standart yok. ElectricSQL kapanırsa (muhtemelen kapanmayacaktır, ancak eğer olursa) PowerSync'e geçiş önemsiz değildir. Senkronizasyon katmanımı kısmen bu nedenle soyutluyorum ama yine de beni tedirgin ediyor.

Ayrıca karmaşıklık bütçesinden de endişeleniyorum. Önce yerel, gerçek bir mimari karmaşıklık katar: senkronizasyon motorları, çakışma çözümü, istemci tarafı geçişleri, kısmi çoğaltma ve senkronizasyon sınırında kimlik doğrulama. Doğru türde bir uygulama geliştiren deneyimli geliştiricilerden oluşan bir ekip için bu karmaşıklık, karşılığını kat kat amorti eder. Sadece CRUD uygulamasına ihtiyaç duyan bir ekip için bu bir tuzak.

Geçen yıl Berlin'de yerel bir ilk buluşmada Kevin adında bir geliştiricinin bana söylediği bir şeye dönüp duruyorum:

"En iyi mimari, ekibinizin gece saat 2'de hata ayıklayabildiği mimaridir."

O haklı. Yerel öncelikli uygulama, uygulamanızı kullanıcılar için daha hızlı, daha güvenilir ve daha iyi hale getiriyorsa ve ekibiniz senkronizasyonun nasıl çalıştığını anlıyorsa bunu yapın. Kulağa hoş geldiği için ekliyorsanız ve arıza modlarını henüz tam olarak anlamadıysanız, önce bir prototip oluşturun. Nerede kırıldığını öğrenin. O zaman karar ver.

Şu anda dördüncü yerel öncelikli uygulamamı geliştiriyorum: küçük ekipler için, çevrimdışı destek ve isteğe bağlı E2E şifrelemeye sahip, işbirliğine dayalı bir planlama aracı. Bu mimariyle denediğim en iddialı şey bu. Nasıl gittiğini yazacağım.

Yeni başlıyorsanız mevcut uygulamanızda anında yerel okuma ve çevrimdışı yazma özelliklerinden yararlanabilecek bir özellik seçin. Yerel bir SQLite veritabanı ekleyin. Reaktif sorguları bağlayın. Nasıl hissettiğini görün. Sanırım sen de benim verdiğim tepkinin aynısını vereceksin: ah, her zaman böyle olması gerekirdi.

Paylaş: