7 poin oleh GN⁺ 2023-10-19 | 1 komentar | Bagikan ke WhatsApp
  • 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 count di Yjs Map lalu menulis ulang prev + 1 dapat 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 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
  • 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 dan increment(2) dari klien 2 terjadi bersamaan, eksekusi server akan membuat hitungan akhir sesuai urutan kedatangannya
    • Tanpa pengetahuan terpisah tentang apa yang dilakukan increment atau 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
    • setHighScore menyimpan nilai yang lebih besar antara high-score lama dan skor kandidat
    • Sebagian besar manipulasi daftar juga bekerja
    • append menambahkan item ke akhir daftar belanja
    • insertAt menyisipkan item di posisi tertentu, dan splice() mengoreksi posisinya
    • Karena indeks pada remove dapat berubah, item atau ID stabil harus diberikan sebagai argumen
    • Invariant tingkat atas juga dapat ditegakkan
    • addChild memperbarui childIDs milik induk dan parentID milik 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.canEdit dan melempar error unauthorized jika 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

 
GN⁺ 2023-10-19
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!”

    • Mereka mulai menyusun kata FUCK dari potongan-potongannya, dan ketika orang lain menyadarinya lalu ikut bergabung, pengalaman itu ternyata terasa cukup hangat
    • Ada bug visual yang memungkinkan menyembunyikan potongan e di belakang bagian e yang sudah selesai
      Untuk huruf lain, garis luarnya tetap terlihat, jadi tidak bisa dilakukan dengan cara yang sama
    • Demo ini sepertinya bukan contoh yang bagus untuk menunjukkan kemampuan library tersebut
      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
    • Ini benar-benar pengalaman yang menyenangkan
      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

  • 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

    • Catatan itu valid
      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
    • Tanpa perlu melibatkan penghapusan pun, jika “secara bersamaan” A menambahkan “a” ke daftar L, B menambahkan “b”, dan C menambahkan “c”, server akan menerapkan penambahan dalam urutan sembarang lalu mengirim hasilnya ke klien
      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”
    • Sebagian besar sistem seperti ini menangani penghapusan sebagai tombstone
      Dengan begitu, penyisipan dapat dilakukan dengan aman setelah atau di antara item yang telah dihapus
    • Dalam aplikasi kolaboratif, server bisa menyelesaikan tiga operasi itu dalam urutan apa pun
      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

    • Aaron orangnya rendah hati, jadi biar saya yang mengatakan: dia membuat Greasemonkey dan selama bertahun-tahun terlibat dalam berbagai inovasi browser yang terobosan
    • Dulu saya membuat beberapa sistem sinkronisasi multiplayer di codebase tertutup, jadi saya mengikuti bidang ini
      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
    • Saya penasaran bagaimana kelanjutan pengembangan Replicache
      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
    • Saya penasaran pendapat Anda tentang ElectricSQL
    • Saya penasaran kira-kira berapa jumlah pengguna maksimum yang bisa masuk ke satu room secara bersamaan
  • 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

    • Bisa saja begitu, tetapi “user” terdengar seperti konsumen
      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
    • Logika ini didasarkan pada logika yang sudah dipakai selama puluhan tahun di game multiplayer
      Sebaliknya, semua software web itu multi-user, jadi istilah itu saja tidak memberi informasi apa pun
    • Belakangan istilahnya memang sudah mengerucut seperti itu
      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

    • Mengatakan “tidak bisa” itu terlalu kuat
      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
    • CRDT juga bisa menyertakan server; hanya saja server tidak harus selalu berada tepat di tengah semuanya
      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
    • Otorisasi bisa ditangani dengan menandatangani setiap operasi atau pesan dan memverifikasi kecocokannya dengan public key dalam daftar kontrol akses
      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 baik

    • Untuk saat ini, belum ada jawaban yang lebih baik daripada tidak menghapus mutator lama sampai kita tahu tidak ada lagi klien yang memiliki perubahan tertunda
      Karena persistensi belum diaktifkan, saat ini periode tersebut cukup singkat
  • Selamat atas peluncurannya
    Saya penasaran apakah ini masih satu keluarga dengan https://partykit.io/

    • Ada di ranah yang sama
      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

    • Lebih tepatnya, karena klien tidak menunggu respons server sebelum menampilkan perubahan lokal, ini lebih dekat ke deterministic rollback
      Ada presentasi GDC yang sangat bagus yang menjelaskan secara mendalam implementasi Mortal Kombat dan Injustice 2: https://youtu.be/7jb0FOcImdg
    • Ini bukan deterministic lockstep
      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