- Reflect, Figma, Notion ve Google Sheets gibi iş birliği uygulamalarını hızlıca oluşturmak için geliştirilmiş bir framework; Replicache’in oyun tarzı senkronizasyon motoruna tam yönetilen bir sunucu eklenerek yayımlandı
- İş birliği arayüzleri, sunucu yanıtını beklemeden yerel değişiklikleri anında göstermeli; bu yüzden aynı veriler eşzamanlı düzenlendiğinde çakışmaların nasıl ele alındığı ürün deneyimini belirler
- Reflect, CRDT yerine Transactional Conflict Resolution yaklaşımını seçer: istemci ve sunucu aynı mutator çağrı geçmişini çalıştırır, sunucu da geliş sırasına göre otoritatif durumu oluşturur
- Yjs gibi sequence CRDT’ler metin, liste ve map yapılarında güçlüdür; ancak sayaç gibi ayrı birleştirme kuralları gerektiren durumlarda artışlar kaybolabilir. Reflect, mutator’ları yeniden çalıştırarak aritmetik işlemleri, liste manipülasyonlarını ve üst seviye değişmezleri ele alır
- Sunucu yalnızca mutation adını ve argümanlarını alıp sonucu yeniden hesapladığı için, istemcinin hesapladığı sonuca güvenmeden yetki kontrolü, şema doğrulama ve migration süreçlerini tasarıma dahil etmek kolaydır
Reflect’in sunduğu geliştirici deneyimi
- Reflect, Figma, Notion ve Google Sheets gibi çok oyunculu web uygulamaları oluşturmak için yeni bir yaklaşımdır
- Mevcut istemci tarafı senkronizasyon framework’ü Replicache’in evrimleşmiş hâlidir ve aynı oyun tarzı senkronizasyon motorunu kullanır
- Replicache’ten farklı olarak tam yönetilen bir sunucu içerir; amacı, yüksek kaliteli çok oyunculu uygulamaların dakikalar içinde oluşturulmasını sağlamaktır
- Reflect ilk kez herkese açık olarak sunuluyor; reflect.net üzerinden tanıtımı görebilir, hello.reflect.net üzerinden başlayabilirsiniz
İş birliğine dayalı düzenlemede çakışmaların ortaya çıkma yapısı
- İş birliğine dayalı düzenlemede kaçınılmaz olarak çakışmalar oluşur
- Anında tepki veren bir arayüz oluşturmak için sunucu beklenemez; değişiklikler önce istemcide yerel olarak gerçekleşmelidir
- Birden fazla kullanıcı aynı öğeyi aynı anda düzenleyebileceği için, tüm kullanıcıların aynı sonucu görmesini sağlayacak şekilde çakışmaların senkronize edilmesi ve doğal biçimde çözülmesi gerekir
- Senkronizasyon motoru; geliştirici deneyimini, kullanıcı deneyimini, mümkün olan performansı ve yapılabilecek uygulama türlerini bile belirler
CRDT ve Yjs sayaç örneği
- Web ekosisteminde CRDT, veri senkronizasyon yöntemi olarak yaygın kullanılır
- CRDT, iş birliği yapanlar arasında değişikliklerin tamamı paylaşıldığında aynı değere yakınsayan bir veri yapısıdır; Yjs ve Automerge başlıca açık kaynak CRDT kütüphaneleridir
- Reflect bir CRDT değildir; video oyun sektöründe uzun süredir kullanılan Server Reconciliation yaklaşımının bir varyasyonu olan Transactional Conflict Resolution kullanır
-
Yjs’te basit bir sayacın neden bozulduğu
- Yjs Map içinde
countdeğerini saklayıpprev + 1sonucunu tekrar yazmak, eşzamanlılık durumunda artışların kaybolmasına neden olabilir - Yjs dokümantasyonundaki doğru sayaç örneği, sayıları bir diziye ekleyip toplamı hesaplama yöntemidir
- Yjs bir sequence CRDT olduğu için listeler, metin parçaları ve map yapılarında güçlüdür; ancak sayaçları doğal biçimde modellemek zordur
- Yjs Map’in birleştirme algoritması anahtar bazında last-write wins yaklaşımını kullanır; iki kullanıcı aynı anda artırma yaptığında değişikliklerden biri kaybolabilir
- CRDT belirli problemlere iyi uyar, ancak o problem sınıfının dışında genişletilmesi zor olan bir sınıra sahiptir
- Yjs Map içinde
Transactional Conflict Resolution nasıl çalışır?
- Reflect’te değişiklikler, mutator adı verilen özel JavaScript fonksiyonlarıyla uygulanır
- Her mutator’ın bir kopyası tüm istemcilerde ve sunucuda bulunur
- Kullanıcı bir değişiklik yaptığında Reflect, mutator çağrı kaydı olan bir mutation oluşturur
- Mutation içinde yalnızca
increment(delta: 1)gibi mutator adı ve argümanlar yer alır - Ortaya çıkan değişiklik içeriği mutation’a dahil edilmez
- Mutation içinde yalnızca
- Reflect, mutation’ı anında yerelde uygular ve arayüzü günceller; kullanıcı kendi değişikliğini hemen görebilir
-
Sunucu doğrusal hâle getirme ve yeniden çalıştırma
- Her istemci sunucuyu beklemeden mutation eklemeye devam eder
- Mutation’lar sunucuya stream edilir; sunucu, mutation’ları geliş zamanına göre doğrusal hâle getirir ve ardından bir sonraki otoritatif durumu oluşturur
- Örneğin istemci 1’in
increment(1)ve istemci 2’ninincrement(2)çağrıları aynı anda gerçekleşirse, sunucu çalıştırmasında nihai sayaç değeri geliş sırasına göre oluşur - Sunucu,
increment’in ne yaptığına veya nasıl birleştirileceğine dair ek bilgiye ihtiyaç duymadan yürütme geçmişini doğrusal hâle getirerek çakışmaları birleştirir - En güncel otoritatif durum her istemciye sürekli olarak stream edilir
- İstemci, bekleyen kendi mutation’ının otoritatif duruma uygulandığını anladığında onu yerel kuyruktan kaldırır
- Kalan bekleyen mutation’lar, en güncel otoritatif durumun üzerinde mutator kodu yeniden çalıştırılarak rebase edilir
- Bu döngünün tamamı istemci başına saniyede en fazla 120 kez gerçekleşir
Uygulama maliyeti ve genelleşen avantajlar
- Bu yaklaşımı uygulamak için geri sarma, fork ve branch oluşturabilen hızlı bir veri deposu gerekir
- Sunucu tarafında da gelen mutation’ları işleyebilen hızlı bir depolama gerekir
- Mutator senkronizasyonu yöntemi ve istemci ya da sunucunun senkronizasyon sırasında çakıştığında toparlanmasını sağlayan işlemler gerekir
- Buna karşılık, keyfi fonksiyonların doğrusal hâle getirilmesi oldukça genel amaçlı bir senkronizasyon stratejisi olarak çalışır
-
Ayrı senkronizasyon kodu olmadan ele alınan örnekler
- Aritmetik işlemler doğal biçimde ele alınır
setHighScore, mevcuthigh-scoreile aday skor arasından büyük olanı saklar- Liste manipülasyonlarının çoğu da çalışır
append, alışveriş listesinin sonuna öğe eklerinsertAt, belirtilen konuma öğe ekler;splice()konumu düzeltirremove, indeks değişebileceği için argüman olarak öğeyi veya kararlı bir ID’yi almalıdır- Daha üst seviye değişmezler de zorunlu kılınabilir
addChild, ebeveyninchildIDsdeğeri ile çocuğunparentIDdeğerinin her zaman tutarlı kalması için ikisini birlikte günceller- Bu örnekler, senkronizasyonu dikkate alan ayrı bir kod olmadan da makul biçimde birleştirilir
Sunucu otoritesi ve yetki kontrolü
- Reflect’te sunucu otoritedir
- İstemcinin değişiklik sonucunu nasıl gördüğü sunucuya veya diğer istemcilere paylaşılmaz
- Sunucuya gönderilenler yalnızca mutation adı ve argümanlarıdır; sunucu mutation sonucunu doğrudan yeniden hesaplar
- Sunucunun istemciyle aynı kodu çalıştırması da gerekmez; harici servisleri sorgulayabilir veya rastgele sayı kullanabilir
-
İnce taneli yetkilendirme
- Bu tasarımda ince taneli yetki kontrolleri doğal biçimde eklenebilir
- İş birliğine dayalı bir tasarım programında konuklara yorum ve vurgulama izni verilip gerçek tasarım değişikliklerinin yasaklanması buna örnektir
- CRDT’de yetkisiz değişiklikleri reddedecek mantığın konumlandırılacağı bir yer olmadığı için bunu uygulamak zordur
- Reflect’te sunucuda çalışan mutator,
tx.user.canEditgibi değerleri kontrol edip yetki yoksaunauthorizedhatası fırlatabilir - Mutator’ın sunucuda istemciden farklı kod çalıştırması sorun değildir; nihai kararı sunucu verir
Şema doğrulama ve kullanım yönlendirmesi
- Reflect’in yaklaşımında şema doğrulama ve migration süreçleri de tasarıma doğal biçimde dahil edilebilir
- Senkronizasyon stratejisi seçimi, çok oyunculu sistemlerin merkezindedir; Reflect, oyun sektöründen öğrenilen Transactional Conflict Resolution’ı basit, esnek ve güçlü bir yaklaşım olarak görür
- Çok oyunculu bir uygulama geliştiriyorsanız Reflect başlangıç sayfası üzerinden deneyebilirsiniz
- Geliştirme ekibiyle konuşmak için discord.reflect.net veya @hello_reflect kullanılabilir
1 yorum
Hacker News yorumları
Ana sayfanın üst kısmındaki demo (https://reflect.net/) oldukça eğlenceli
İzlerken, bulmaca her tamamlandığında insanların “başardık!” der gibi imleçlerini sallayıp sevindiğini görüyorsunuz
Diğer harflerde dış çizgi görünmeye devam ettiği için aynı yöntem işe yaramıyor
Bir parçayı tuttuğunuzda o parça o kullanıcıya kilitlenmiş gibi görünüyor; bu yüzden çözülecek neredeyse hiç çakışma yok, en fazla iki kişi aynı anda aldığında önce alana verilmesi kalıyor
Gerçekten çakışma çözümlemenin gerçekleştiği daha iyi bir demo görmek isterdim
Anlatılan sahneyi gösteren video burada: https://streamable.com/asu261
Daha önce Replicache adıyla bir iki kez gündeme geldiğini hatırlıyor olabilirsiniz
Reflect, buna tamamen yönetilen ve çok hızlı bir senkronizasyon sunucusu eklenmiş hâli
Yerel öncelikli/gerçek zamanlı alanı bugünlerde kalabalık, ama Replicache/Reflect veri modeli ve kodlama modeli açısından zarif biçimde basit olduğu için bakmaya değer
CRDT’ye kıyasla avantajı, çakışmaları basit sıralı kodla doğrudan ele alabilmeniz; yerleşik kurallar uymadığında CRDT’ye uygulamaya özel çakışma çözümleme eklemenin karmaşık olabileceğini düşünüyorum
PowerSync de CRDT yerine sunucu uzlaştırma mimarisini seçti; merkezi sunucusu olan uygulamalarda bu yapının sadeliği bence çok çekici
Sunucu uzlaştırma kavramını tanıtıp yaygınlaştırdığı için Aaron’a hakkını vermek isterim: https://www.gabrielgambetta.com/client-side-prediction-serve...
A JavaScript framework to build offline-first but collaborative webapp - https://news.ycombinator.com/item?id=33269440 - Ekim 2022
Linear clone, with realtime sync and instant UI - built with Replicache - https://news.ycombinator.com/item?id=31331660 - Mayıs 2022
Replicache: Easy Offline-First for Existing Applications - https://news.ycombinator.com/item?id=22173500 - Ocak 2020
Yaklaşık 15 yıl önce büyükannemin evinde can sıkıntısından kaçmak için bu konuyu epey derinlemesine kurcalamış, bir JavaScript uygulaması da yazmıştım
“Aritmetik işlemler kendiliğinden çalışır” demek elbette idempotent işlemler hâline getirmek açısından çok kolay; ama “liste işlemleri de kendiliğinden çalışır” ifadesine katılmak zor
Örneğin [1, 2, 3, 4, 5] gibi bir dizide A’nın 2~4 aralığını sildiğini, B’nin 3~5 aralığını sildiğini, C’nin de 3 ile 4 arasına bir şey eklediğini düşünün; üç güncelleme sunucuya aynı anda ulaştığında çözüm belirsizleşiyor
Zaman damgasına ya da son yazan kazanır (Last Write Wins) yaklaşımına yaslanırsanız işlem modeli bozulur; birini kazanan seçerseniz diğer kullanıcıların ne göreceği ve bunun nasıl aktarılacağı da sorun olur
Yanıt “tüm diziyi yeniden gönder” ise yine işlem modeli bozulur
Aslında kastettiğimiz ifade daha çok “birçok liste işlemi kendiliğinden çalışır” idi
Reflect’te düzenleme/silme tanımlayıcısı olarak liste indeksi kullanamazsınız; çünkü indeksler kararlı değildir. Atomikse öğenin kendisini, genellikle de kararlı bir ID kullanın dememizin nedeni bu: https://i.imgur.com/IKzmf0q.png
C’nin eklediği şeyin silinmesi sorunu da var; ancak gerçek zamanlı işbirliği bağlamında hiçbir protokol bunu tamamen çözemez
C açısından az önce yazdığı içeriğin kaybolması üzücü olabilir; bu, aynı alanda eşzamanlı çalışan insanların niyetlerinin farklı olmasından doğan bir durumdur ve geri alma ile o anda kimin çalıştığını gösterme gibi sosyal mekanizmalar yardımcı olur
Bu sırada her istemci kendi eklemesini yerelde uygulamış olabilir; A [“a”], B [“b”], C [“c”] görür. Sunucu [“c”, “b”, “a”] durumunu gönderdiğinde istemciler bekleyen değişikliklerini atıp sunucu durumunu dünyanın gerçeği olarak kabul edecek gibi görünüyor
Ancak her ekleme “benim değişikliğim önce uygulanırsa kazanırım” gibi bir etki yaratıyorsa, sunucu güncellemesini beklenen 300 ms boyunca herkesin “you win” görüp görmeyeceğini merak ediyorum
Böylece silinmiş öğelerin arkasına ya da arasına güvenle ekleme yapabilirsiniz
Üç kullanıcı sonucu görüp aynı veriye aynı anda dokunduklarını ve durumun karıştığını fark eder, sonra düzeltir
Belge düzenlemeyi düşünün
Birbirinizin çalıştığını göremiyorsanız şaşırabilirsiniz, ama sonunda istediğiniz duruma getirebilirsiniz
Sonucu kontrol etmiyor ya da edemiyorsanız durum doğru olmayabilir; ancak işbirlikçi düzenleme veya oyun gibi etkileşimli uygulamalarda akış genellikle böyle olmaz
Bu projede yer alan kişilerden biriyim; sorularınız varsa yanıtlayabilirim
Rebase ve sunucu otoritesine sahip bir “redux-pubsub” kütüphanesi de yapmıştım; anladığım kadarıyla TCR’ye benziyor
Bu modelde hoşuma giden çok şey var ve bağlantı verilen yazı da çok net
“Şema doğrulama ve migration’lar tasarımdan doğal olarak neredeyse bedavaya gelir” denmiş; migration’larda pratikte hangi yöntemin iyi çalıştığını merak ediyorum
Ayrıca TCR sisteminde ciddi miktarda paylaşımlı metin düzenlemeyi kapsayan bir kullanım senaryosunda genelde akla varsayılan olarak Yjs ve Tiptap/ProseMirror geliyor; CRDT dokümanı ile TCR dokümanını paralel tutmanın en iyi yol olup olmadığını merak ediyorum
İstemci tarafındaki kod tabanı büyük ölçüde ortak olduğu için güncellemeleri beklemeye devam edebilir miyiz, yoksa Reflect’in ana odak haline gelip onun yerini alması daha mı olası, bilmek isterim
Terminoloji biraz kafa karıştırıcı
Oyunlarda “oyuncu” olduğu için senkronizasyon sistemine “çok oyunculu” deniyor, ama genel yazılımlarda “kullanıcı” var; bu yüzden çok kullanıcılı demek daha doğru görünüyor
Sayfada “kullanıcı” ve “çok oyunculu” ifadeleri birlikte kullanıldığı için garip okunuyor
HN çok kullanıcılı bir servis, ama HN’ye çok oyunculu demek tuhaf olur
Gerçek zamanlı eşzamanlı etkileşim, çok kullanıcılıdan daha güçlü bir şey; birden fazla kullanıcının eylemde bulunup etkileştiği anlamındaki çok oyunculu bence yerinde bir kullanım
Öte yandan tüm web yazılımları çok kullanıcılıdır; bu ifade tek başına hiçbir bilgi vermez
Çok oyunculu, diğer kullanıcıların her an ne yaptığını görselleştiren canlı çok kullanıcılı deneyim anlamına geliyor
Yaklaşık 2 yıldır CRDT’lere yüzeysel olarak bakıyorum ve hep yetkilendirme nasıl oluyor diye merak ettim
Bu yazı, Y.js gibi CRDT kütüphanelerinin tek başına değişikliklerin yetki kontrolünden geçmesi gereken uygulamaları düzgün şekilde ele almakta zorlandığını ima ediyor gibi
Bunun nedeni merkezi bir otoritenin, yani sunucunun olmaması; Reflect ise sunucunun istemci etkileşimlerine aracılık ettiğini varsayıyor gibi görünüyor. Bu anlayış doğru mu, merak ediyorum
Uğraşılırsa CRDT’lerde de yetki yönetimi yapılabilir; örneğin her şeyin arasına bir sunucu koyup, sunucunun gördüğü yetkisiz değişiklikleri geri aldırabilirsiniz
Ama uygulama büyüdükçe bakım yükü artar ve kırılgan hale gelir; üstelik CRDT’nin avantajlarının bir kısmı da baştan kaybolur
Sunucu zaten aradaysa, en baştan sunucunun mesajları reddedebildiği bir protokol kullanmak çok daha basittir
Örneğin bağlantı kimlik doğrulamasını sunucu üstlenebilir
Birisi P2P bağlanıp belirli bir yetkiye sahip olduğunu iddia ederse, bu iddia sunucuyla doğrulanabilir
Ayrıca CRDT mutlaka P2P anlamına gelmez; merkezi sunucu üzerinden mesaj iletimi yaparken de mevcut durumu hem sunucu hem istemci tarafında çözen CRDT modelini koruyabilirsiniz
Bu yöntem CRDT’nin P2P bağlamında da çalışmasını sağlar
Ancak sunucu otoriteyse, yetkisiz istemcinin mesajını doğrudan reddetmek yeterlidir
mutator yükseltmeleri nasıl ele alınıyor, merak ediyorum
İstemci eski kodu çalıştırıyorsa operasyon sunucudakinden farklılaşacaktır; bariz cevap
increment_v1,increment_v2gibi bağımsız sürümlemek, ama daha iyi bir yöntem var mı merak ediyorumHenüz kalıcılığı açmadığımız için şu anda bu süre oldukça kısa
Lansman hayırlı olsun
https://partykit.io/ ile benzer bir kategoride mi, merak ediyorum
Temel fark, her birinin ne kadar kanaatli bir tasarıma sahip olduğu
PartyKit çok az kural koyan bir yapı; daha çok hızlı başlanabilen ve otomatik ölçeklenen hafif bir JavaScript sunucusuna yakın
Görünüşe göre çoğu kişi PartyKit üzerinde yjs çalıştırıyor, ama automerge veya Replicache de çalıştırılabilir
Reflect ise mümkün olan en iyi çok oyunculu deneyimi sunmaya tamamen odaklanıyor; çok oyunculunun kendiliğinden çalışmasını sağlamak ve uygulama geliştirmeye odaklanmanızı mümkün kılmak için yığın genelinde birçok tercihi güçlü biçimde entegre etmeyi planlıyor
Bu arada, bu strateji oyun geliştirme tarafında deterministik lockstep olarak adlandırılır ve durumu senkronize etmesi gereken çok sayıda entity bulunan oyunlarda özellikle sık kullanılır.
Tipik örnek, neredeyse tamamı bu yöntemi kullanan gerçek zamanlı strateji oyunlarıdır.
Mortal Kombat ve Injustice 2 uygulamasını derinlemesine anlatan mükemmel bir GDC sunumu var: https://youtu.be/7jb0FOcImdg
Deterministik lockstep, her katılımcının oyun simülasyonunu ilerletmeden önce diğer tüm oyuncuların girdilerini beklediği bir P2P oyun algoritmasıdır.
“Deterministik” denmesinin nedeni simülasyon sonucunun değil girdilerin paylaşılması ve aynı girdiler için simülasyonun deterministik olmasıdır; “lockstep” denmesinin nedeni ise tüm istemcilerin koordine edilmiş bir hızda ilerlemesidir.
Age of Empires serisi bu yöntemi kullandığı için tıkladığınızda birimler hemen hareket etmez; StarCraft da bunu kullanır, ancak oynanış hissini daha akıcı kılmak için bazı hilelere sahiptir.
Reflect, P2P değil, sunucu otoriteli simülasyona daha yakındır.
İstemci girdiyi sunucuya gönderir ama sonucu beklemeden yerelde tahmin yapar; sunucu ise tek tek istemci gecikmelerini telafi etmek için zamanı geri alır ve girdileri yeniden oynatır.
Ardından istemci, kendi girdisini içeren sunucu sonucunu aldığında yerel simülasyonu düzeltir.
Bu algoritmanın anahtar terimleri sunucu otoritesi, tahmin, gecikme telafisi ve tahmin düzeltmesidir.
Reflect'te var mı bilmiyorum, ancak FPS oyunlarında dünya güncellemesi alındığında entity'leri belirli bir süre boyunca yeni konum ve dönüşlerine enterpole eden istemci tarafı enterpolasyon da yaygındır.
Tek otorite sunucu olduğu için determinizm çok kritik değildir, fakat istemci tahmininde hatalı tahminleri azaltmada faydalıdır.
Hatalı tahminler, diğer istemci girdilerinin dünya durumunu büyük ölçüde değiştirmesi veya rastgele sayı üretiminin senkronize olmaması gibi simülasyonun deterministik olmadığı durumlarda ortaya çıkar.
Counter Strike'ta “nospread” hilesini engellemek için mermi saçılımı rastgeleliğini senkronize etmeyen örnekler de vardır.
https://www.gabrielgambetta.com/client-side-prediction-live-...
https://developer.valvesoftware.com/wiki/Latency_Compensatin...
https://developer.valvesoftware.com/wiki/Source_Multiplayer_...
Reflect harika.
Şu anda prodüksiyonda alfa sürümünü kullanıyoruz; yalnızca sistemden değil, Aaron ve ekipten de çok memnunuz.
Bir müşteri olarak sorularınız varsa yanıtlayabilirim.