- Reflect dirilis sebagai framework untuk membuat aplikasi kolaboratif seperti Figma, Notion, dan Google Sheets dengan cepat, dengan menambahkan server terkelola penuh ke mesin sinkronisasi bergaya game milik Replicache
- Karena UI kolaboratif harus langsung menampilkan perubahan lokal tanpa menunggu respons server, cara menangani konflik saat data yang sama diedit secara bersamaan sangat menentukan pengalaman produk
- Alih-alih CRDT, Reflect memilih Transactional Conflict Resolution: klien dan server menjalankan riwayat pemanggilan mutator yang sama, lalu server membuat state otoritatif berdasarkan urutan kedatangan
- Sequence CRDT seperti Yjs kuat untuk teks, daftar, dan map, tetapi pada kasus yang memerlukan aturan merge terpisah seperti counter, increment bisa hilang; Reflect menangani aritmetika, manipulasi daftar, dan invariant tingkat atas dengan menjalankan ulang mutator
- Karena server hanya menerima nama mutation dan argumen lalu menghitung ulang hasilnya, desainnya memudahkan untuk memasukkan pemeriksaan izin, validasi skema, dan migrasi tanpa memercayai hasil komputasi klien
Pengalaman pengembangan yang diperkenalkan Reflect
- Reflect adalah cara baru untuk membuat aplikasi web multiplayer seperti Figma, Notion, dan Google Sheets
- Ini merupakan evolusi dari framework sinkronisasi sisi klien yang sudah ada, Replicache, dan menggunakan mesin sinkronisasi bergaya game yang sama
- Berbeda dari Replicache, Reflect menyertakan server terkelola penuh, dengan tujuan memungkinkan pembuatan aplikasi multiplayer berkualitas tinggi dalam hitungan menit
- Reflect tersedia untuk umum untuk pertama kalinya; Anda dapat membaca pengantarnya di reflect.net dan mulai di hello.reflect.net
Struktur yang menimbulkan konflik dalam pengeditan kolaboratif
- Dalam pengeditan kolaboratif, konflik pasti terjadi
- Untuk membuat UI yang langsung responsif, aplikasi tidak bisa menunggu server, dan perubahan harus terjadi lebih dulu secara lokal di klien
- Karena beberapa pengguna dapat mengedit item yang sama secara bersamaan, konflik harus disinkronkan dan diselesaikan secara alami agar semua pengguna melihat hasil yang sama
- Mesin sinkronisasi memengaruhi pengalaman developer, pengalaman pengguna, performa yang mungkin dicapai, hingga jenis aplikasi yang dapat dibuat
Contoh counter pada CRDT dan Yjs
- Di ekosistem web, CRDT banyak digunakan sebagai metode sinkronisasi data
- CRDT adalah struktur data yang akan konvergen ke nilai yang sama setelah semua perubahan dipertukarkan di antara kolaborator; Yjs dan Automerge adalah pustaka CRDT open source yang representatif
- Reflect bukan CRDT, melainkan menggunakan Transactional Conflict Resolution, variasi dari Server Reconciliation yang sudah lama digunakan di industri video game
-
Mengapa counter sederhana rusak di Yjs
- Cara menyimpan nilai
countdi Yjs Map lalu menulis ulangprev + 1dapat kehilangan increment dalam situasi konkurensi - Contoh counter yang benar di dokumentasi Yjs adalah menambahkan angka ke array lalu menghitung totalnya
- Karena Yjs adalah sequence CRDT, ia kuat untuk daftar, potongan teks, dan map, tetapi sulit memodelkan counter secara natural
- Algoritme merge Yjs Map memakai pendekatan last-write wins per key, sehingga jika dua pengguna menaikkan nilai secara bersamaan, salah satu perubahan bisa hilang
- CRDT cocok untuk masalah tertentu, tetapi memiliki keterbatasan karena sulit diperluas saat masalahnya berada di luar bentuk tersebut
- Cara menyimpan nilai
Cara kerja Transactional Conflict Resolution
- Di Reflect, perubahan diimplementasikan sebagai fungsi JavaScript khusus yang disebut mutator
- Salinan setiap mutator ada di semua klien dan server
- Saat pengguna membuat perubahan, Reflect membuat mutation, yaitu riwayat pemanggilan mutator
- mutation hanya berisi nama mutator dan argumen, seperti
increment(delta: 1) - Perubahan yang dihasilkan tidak disertakan di dalam mutation
- mutation hanya berisi nama mutator dan argumen, seperti
- Reflect langsung menerapkan mutation secara lokal untuk memperbarui UI, sehingga pengguna dapat segera melihat perubahannya sendiri
-
Linearisasi dan eksekusi ulang di server
- Setiap klien terus menambahkan mutation tanpa menunggu server
- mutation di-stream ke server, lalu server melinearisasi mutation berdasarkan urutan waktu kedatangan dan menghasilkan state otoritatif berikutnya
- Misalnya, jika
increment(1)dari klien 1 danincrement(2)dari klien 2 terjadi bersamaan, eksekusi server akan membuat hitungan akhir sesuai urutan kedatangannya - Tanpa pengetahuan terpisah tentang apa yang dilakukan
incrementatau bagaimana menggabungkannya, server menggabungkan konflik dengan melinearisasi riwayat eksekusi - State otoritatif terbaru terus di-stream ke setiap klien
- Saat klien mengetahui bahwa mutation yang sedang menunggu miliknya telah diterapkan ke state otoritatif, mutation tersebut dihapus dari antrean lokal
- Mutation tertunda yang tersisa kemudian di-rebase dengan menjalankan ulang kode mutator di atas state otoritatif terbaru
- Seluruh siklus ini terjadi hingga 120 kali per detik untuk setiap klien
Biaya implementasi dan keunggulan yang dapat digeneralisasi
- Untuk mengimplementasikan pendekatan ini, dibutuhkan datastore cepat yang dapat melakukan rewind, fork, dan membuat branch
- Di sisi server juga dibutuhkan storage cepat untuk memproses mutation yang masuk
- Diperlukan cara untuk menyinkronkan mutator serta penanganan pemulihan ketika klien atau server mengalami konflik selama sinkronisasi
- Sebagai imbalannya, linearisasi fungsi arbitrer bekerja sebagai strategi sinkronisasi yang cukup umum
-
Contoh yang ditangani tanpa kode sinkronisasi terpisah
- Operasi aritmetika ditangani secara natural
setHighScoremenyimpan nilai yang lebih besar antarahigh-scorelama dan skor kandidat- Sebagian besar manipulasi daftar juga bekerja
appendmenambahkan item ke akhir daftar belanjainsertAtmenyisipkan item di posisi tertentu, dansplice()mengoreksi posisinya- Karena indeks pada
removedapat berubah, item atau ID stabil harus diberikan sebagai argumen - Invariant tingkat atas juga dapat ditegakkan
addChildmemperbaruichildIDsmilik induk danparentIDmilik anak secara bersamaan agar selalu konsisten- Contoh-contoh ini di-merge secara masuk akal tanpa kode khusus yang menyadari sinkronisasi
Otoritas server dan pemeriksaan izin
- Di Reflect, server adalah otoritas
- Bagaimana klien memandang hasil perubahan tidak dibagikan ke server maupun klien lain
- Yang dikirim ke server hanyalah nama mutation dan argumennya, lalu server menghitung sendiri hasil mutation
- Server juga tidak harus menjalankan kode yang sama dengan klien, dan dapat memanggil layanan eksternal atau menggunakan angka acak
-
Otorisasi terperinci
- Dalam desain ini, pemeriksaan izin terperinci dapat dimasukkan secara natural
- Contohnya adalah pada program desain kolaboratif yang mengizinkan tamu memberi komentar dan highlight, tetapi melarang perubahan desain yang sebenarnya
- Pada CRDT, sulit mengimplementasikan penolakan perubahan tanpa izin karena tidak ada tempat yang jelas untuk menaruh logika tersebut
- Di Reflect, mutator yang berjalan di server dapat memeriksa nilai seperti
tx.user.canEditdan melempar errorunauthorizedjika tidak memiliki izin - Tidak masalah jika mutator menjalankan kode yang berbeda di server dan klien; serverlah yang membuat keputusan akhir
Validasi skema dan panduan penggunaan
- Dalam pendekatan Reflect, validasi skema dan migrasi juga dapat dimasukkan secara natural ke dalam desain
- Pemilihan strategi sinkronisasi adalah inti dari sistem multiplayer, dan Reflect memandang Transactional Conflict Resolution yang dipelajari dari pendekatan industri game sebagai cara yang sederhana, fleksibel, dan kuat
- Jika Anda sedang membuat aplikasi multiplayer, Anda dapat mencobanya di halaman awal Reflect
- Untuk berbicara dengan tim pengembang, Anda dapat menggunakan discord.reflect.net atau @hello_reflect
1 komentar
Komentar Hacker News
Demo di bagian atas halaman utama (https://reflect.net/) cukup menyenangkan
Saat menontonnya, setiap kali puzzle selesai, orang-orang tampak senang sambil menggoyang-goyangkan kursor seolah berkata “kita berhasil!”
Untuk huruf lain, garis luarnya tetap terlihat, jadi tidak bisa dilakukan dengan cara yang sama
Saat sebuah potongan diambil, potongan itu tampak terkunci untuk pengguna tersebut, sehingga hampir tidak ada konflik yang perlu diselesaikan; paling-paling hanya soal memberikannya kepada pengguna yang mengambil lebih dulu ketika dua orang mengambilnya bersamaan
Saya ingin melihat demo contoh yang lebih baik, yang benar-benar memunculkan resolusi konflik
Video yang menampilkan adegan yang dijelaskan ada di sini: https://streamable.com/asu261
Mungkin ada yang ingat ini pernah muncul satu-dua kali sebelumnya sebagai Replicache
Reflect adalah bentuk yang menambahkan server sinkronisasi yang sepenuhnya terkelola dan sangat cepat
Bidang local-first/realtime belakangan ini makin ramai, tetapi Replicache/Reflect layak dilihat karena model data dan model pemrogramannya sangat indah dalam kesederhanaannya
Dibandingkan CRDT, kelebihannya adalah konflik bisa ditangani langsung dengan kode sekuensial yang sederhana, dan menurut saya menambahkan resolusi konflik khusus aplikasi ke CRDT ketika aturan bawaan tidak cocok bisa menjadi rumit
PowerSync juga memilih arsitektur rekonsiliasi server alih-alih CRDT, dan untuk aplikasi yang memiliki server pusat, kesederhanaan struktur seperti ini terasa sangat menarik
Saya ingin memberi kredit kepada Aaron karena memperkenalkan dan menyebarkan konsep rekonsiliasi server: 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 - Oktober 2022
Linear clone, with realtime sync and instant UI - built with Replicache - https://news.ycombinator.com/item?id=31331660 - Mei 2022
Replicache: Easy Offline-First for Existing Applications - https://news.ycombinator.com/item?id=22173500 - Januari 2020
Sekitar 15 tahun lalu, untuk mengusir bosan di rumah nenek, saya cukup mendalami topik ini dan juga mencoba implementasi JavaScript
Pernyataan “operasi aritmetika akan berjalan begitu saja” tentu sangat mudah dibuat sebagai operasi idempoten, tetapi sulit bagi saya untuk setuju dengan pernyataan “operasi daftar juga akan berjalan begitu saja”
Misalnya, untuk array seperti [1, 2, 3, 4, 5], bayangkan A menghapus rentang 2–4, B menghapus rentang 3–5, dan C menyisipkan sesuatu di antara 3 dan 4; ketika ketiga pembaruan itu tiba di server secara bersamaan, solusinya menjadi tidak jelas
Jika mengandalkan timestamp atau Last Write Wins, model transaksi akan rusak; jika memilih salah satu sebagai pemenang, apa yang akan dilihat pengguna lain dan bagaimana menyampaikannya juga menjadi masalah
Jika jawabannya adalah “kirim ulang seluruh array”, maka model transaksi tetap rusak
Sebenarnya maksudnya lebih dekat ke “banyak operasi daftar akan berjalan begitu saja”
Di Reflect, indeks daftar tidak bisa digunakan sebagai pengenal untuk edit/hapus, karena indeks tidak stabil; inilah alasan disarankan memakai item itu sendiri jika atomik, atau biasanya ID yang stabil: https://i.imgur.com/IKzmf0q.png
Ada juga masalah bahwa penyisipan C bisa terhapus, tetapi dalam konteks kolaborasi realtime, tidak ada protokol yang bisa menyelesaikannya sepenuhnya
Dari sudut pandang C, isi yang baru saja ditulis bisa hilang dan itu bisa membuat sedih; hal seperti ini muncul karena niat manusia yang bekerja bersamaan di ruang yang sama bisa berbeda, sehingga mekanisme sosial seperti undo dan penanda siapa yang sedang mengerjakan apa dapat membantu
Sementara itu, setiap klien bisa saja menerapkan penambahannya sendiri secara lokal sehingga A melihat [“a”], B melihat [“b”], dan C melihat [“c”]; ketika server mengirim status [“c”, “b”, “a”], tampaknya klien akan membuang perubahan yang masih tertunda dan menjadikan status server sebagai kebenaran dunia
Namun jika tiap penambahan memiliki efek seperti “menang jika perubahan saya diterapkan lebih dulu”, saya penasaran apakah selama 300 ms menunggu pembaruan server semua orang akan melihat “you win”
Dengan begitu, penyisipan dapat dilakukan dengan aman setelah atau di antara item yang telah dihapus
Tiga pengguna melihat hasilnya, menyadari bahwa mereka menyentuh data yang sama secara bersamaan dan statusnya menjadi kacau, lalu memperbaikinya
Bayangkan saja penyuntingan dokumen
Jika tidak bisa melihat bahwa satu sama lain sedang bekerja, hal itu memang bisa mengejutkan, tetapi pada akhirnya mereka bisa memperbaikinya ke status yang diinginkan
Jika hasilnya tidak diperiksa atau tidak bisa diperiksa, statusnya tidak akan benar; tetapi dalam aplikasi interaktif seperti penyuntingan kolaboratif atau game, biasanya alurnya tidak seperti itu
Saya salah satu orang yang terlibat dalam proyek ini, dan bisa menjawab jika ada pertanyaan
Saya juga membuat library “redux-pubsub” dengan rebase dan otoritas server, yang sejauh pemahaman saya mirip dengan TCR
Ada banyak hal yang saya suka dari model ini, dan artikel yang ditautkan juga sangat jelas
Disebutkan bahwa “validasi skema dan migrasi nyaris muncul gratis secara alami dari desainnya”; saya penasaran pendekatan apa yang benar-benar efektif untuk migrasi
Selain itu, untuk kasus penggunaan yang menangani cukup banyak penyuntingan teks bersama dalam sistem TCR, biasanya Yjs dan Tiptap/ProseMirror yang langsung terpikir sebagai default; saya penasaran apakah cara terbaiknya adalah menempatkan dokumen CRDT dan dokumen TCR secara paralel
Ingin tahu apakah sebagian besar codebase sisi kliennya dibagi sehingga kami bisa berharap akan terus diperbarui, atau apakah Reflect kemungkinan besar akan menggantikannya sebagai fokus utama
Istilahnya agak membingungkan
Di game ada “player”, jadi masuk akal menyebut sistem sinkronisasinya “multiplayer”, tetapi di software umum ada “user”, jadi rasanya lebih tepat disebut multi-user
Di halaman itu, penggunaan campuran antara “user” dan “multiplayer” terasa canggung saat dibaca
HN adalah layanan multi-user, tetapi aneh menyebut HN sebagai multiplayer
Interaksi serentak real-time punya sesuatu yang lebih kuat daripada sekadar multi-user, dan multiplayer sebagai istilah yang berarti banyak pengguna bertindak dan berinteraksi menurut saya merupakan penggunaan yang masuk akal
Sebaliknya, semua software web itu multi-user, jadi istilah itu saja tidak memberi informasi apa pun
Multiplayer berarti multi-user live yang memvisualisasikan apa yang dilakukan pengguna lain di setiap saat
Selama sekitar 2 tahun saya sekilas mempelajari CRDT dan selalu penasaran bagaimana otorisasi ditangani
Artikel ini tampaknya menyiratkan bahwa library CRDT seperti Y.js saja sulit menangani aplikasi yang perubahannya harus melalui pemeriksaan izin dengan benar
Karena tidak ada otoritas pusat, yaitu server; Reflect tampaknya mengasumsikan server memediasi interaksi klien. Saya penasaran apakah pemahaman ini benar
Dengan usaha, otorisasi juga bisa ditangani di CRDT; misalnya menaruh server di antara semuanya dan membuat server membatalkan perubahan tanpa izin yang dilihatnya
Namun seiring aplikasi membesar, pemeliharaannya meningkat dan menjadi rapuh, dan sebagian keunggulan CRDT sejak awal juga hilang
Jika server sudah berada di tengah, jauh lebih sederhana memakai protokol yang memungkinkan server menolak pesan sejak awal
Misalnya autentikasi koneksi bisa ditangani server
Jika seseorang terhubung secara P2P dan mengklaim izin tertentu, klaim itu bisa diverifikasi ke server
Selain itu, CRDT tidak selalu berarti P2P; kita bisa menyampaikan pesan melalui server pusat sambil tetap mempertahankan model CRDT yang menyelesaikan status saat ini di sisi server maupun klien
Cara ini memungkinkan CRDT tetap bekerja dalam konteks P2P
Namun jika server adalah pihak otoritatif, cukup tolak saja pesan dari klien yang tidak berwenang
Saya penasaran bagaimana upgrade mutator ditangani
Jika klien menjalankan kode lama, operasinya akan berbeda dari server; jawaban yang jelas adalah melakukan versioning secara terpisah seperti
increment_v1,increment_v2, tetapi saya penasaran apakah ada cara yang lebih baikKarena persistensi belum diaktifkan, saat ini periode tersebut cukup singkat
Selamat atas peluncurannya
Saya penasaran apakah ini masih satu keluarga dengan https://partykit.io/
Perbedaan utamanya adalah seberapa opinionated desain masing-masing
PartyKit sangat tidak preskriptif, lebih mirip server JavaScript ringan yang cepat dimulai dan auto-scaling
Sepertinya kebanyakan orang menjalankan yjs di PartyKit, tetapi automerge atau Replicache juga bisa dijalankan
Reflect sepenuhnya berfokus pada penyediaan pengalaman multiplayer terbaik yang memungkinkan, dan rencananya adalah mengintegrasikan banyak pilihan secara kuat di seluruh stack agar multiplayer langsung berfungsi dan Anda bisa fokus pada implementasi aplikasi
Sebagai referensi, strategi ini di dunia pengembangan game disebut deterministic lockstep, dan sangat sering dipakai terutama pada game yang punya banyak entitas yang harus disinkronkan statusnya
Contoh representatifnya adalah game strategi real-time, yang hampir semuanya memakai pendekatan ini
Ada presentasi GDC yang sangat bagus yang menjelaskan secara mendalam implementasi Mortal Kombat dan Injustice 2: https://youtu.be/7jb0FOcImdg
Deterministic lockstep adalah algoritma game P2P; setiap peserta menunggu input dari semua pemain lain, lalu menjalankan simulasi game
Disebut “deterministic” karena yang dibagikan bukan hasil simulasi melainkan input, dan simulasi bersifat deterministik untuk input yang sama; disebut “lockstep” karena semua klien maju dengan laju yang terkoordinasi
Seri Age of Empires memakai pendekatan ini, sehingga unit tidak langsung bergerak saat diklik; StarCraft juga memakainya, tetapi punya trik agar rasa bermainnya terasa lebih mulus
Reflect lebih dekat ke simulasi otoritatif server daripada P2P
Klien mengirim input ke server, tetapi tidak menunggu hasilnya dan melakukan prediksi secara lokal; server memundurkan waktu dan memutar ulang input untuk mengompensasi latensi masing-masing klien
Setelah itu, ketika klien menerima hasil server yang memuat inputnya sendiri, klien mengoreksi simulasi lokalnya
Kata kunci algoritma ini adalah otoritas server, prediksi, kompensasi latensi, dan rekonsiliasi prediksi
Saya tidak tahu apakah ini ada di Reflect, tetapi pada game FPS, interpolasi sisi klien juga umum: ketika menerima update dunia, entitas diinterpolasi ke posisi dan rotasi baru selama jangka waktu tertentu
Karena satu-satunya otoritas adalah server, determinisme tidak terlalu krusial, tetapi berguna untuk mengurangi salah prediksi pada prediksi klien
Salah prediksi terjadi ketika input klien lain mengubah status dunia secara besar, atau ketika simulasi tidak deterministik, misalnya pembangkitan angka acak tidak tersinkronisasi
Counter Strike juga punya contoh tidak menyinkronkan angka acak sebaran peluru untuk mencegah cheat “nospread”
https://www.gabrielgambetta.com/client-side-prediction-live-...
https://developer.valvesoftware.com/wiki/Latency_Compensatin...
https://developer.valvesoftware.com/wiki/Source_Multiplayer_...
Reflect luar biasa
Saat ini kami memakai versi alfa di produksi, dan saya sangat puas bukan hanya dengan sistemnya, tetapi juga dengan Aaron dan timnya
Kalau ada pertanyaan dari sudut pandang pelanggan, saya bisa menjawabnya