4 poin oleh GN⁺ 2025-05-23 | 1 komentar | Bagikan ke WhatsApp
  • Dalam aplikasi kolaboratif berbasis server terpusat, jika teks diedit dengan indeks array, posisi akan bergeser saat penyuntingan bersamaan, sehingga status server diperbarui dengan memberi ID unik global pada setiap karakter dan menyisipkan dengan cara “insert setelah ID tertentu”
  • CRDT dan OT yang dipakai di layanan nyata memang kuat, tetapi algoritme total order atau aturan transformasi operasinya rumit sehingga sulit mengubah cara kerja internalnya agar sesuai dengan kebutuhan aplikasi
  • Pendekatan yang diusulkan membuat klien dan server mempertahankan daftar ID berbentuk Array<{ id: ID; char?: string; isDeleted: boolean }> dan bahkan karakter yang dihapus pun dibiarkan seperti tombstone agar referensi posisi penyisipan berikutnya tidak rusak
  • Pembaruan lokal optimistis ditangani lewat rekonsiliasi server (server reconciliation): saat menerima operasi jarak jauh, operasi lokal yang masih menunggu dibatalkan sementara, lalu operasi jarak jauh dan operasi lokal yang belum diakui diterapkan ulang secara berurutan
  • Artikel ini juga membahas urutan penyisipan serentak, format rich text, varian terdistribusi, hingga pustaka Articulated, serta bagaimana server dapat mendefinisikan operasi yang fleksibel melampaui sekadar insert dan delete sesuai makna aplikasi

Mengapa penyuntingan berbasis indeks rusak saat ada penyuntingan serentak

  • Dalam penyuntingan teks kolaboratif, klien mengirim operasi yang dimasukkan pengguna ke server, dan server harus memperbarui status otoritatif miliknya
  • Jika teks dipandang sebagai array karakter lalu dikirim operasi seperti sisipkan " the" di index 17, indeks yang sama bisa menunjuk ke posisi berbeda karena penyisipan pengguna lain sebelum operasi itu tiba di server
    • Misalnya, jika Alice menyisipkan " gray" di bagian depan, maka index 17 milik Bob bukan lagi posisi semula
    • Server harus melakukan rebase pada operasi Bob menjadi index 22
  • Inti masalahnya adalah operasi apa yang harus dikirim klien ke server, dan bagaimana server harus menafsirkannya agar teks diperbarui dengan cara yang “jelas benar”
  • Masalah rebase indeks ini tidak hanya muncul di aplikasi kolaborasi real-time seperti Google Docs, tetapi juga pada formulir web yang menyisipkan item daftar atau aplikasi lokal single-threaded yang menangani komentar inline dan riwayat edit

Titik berat CRDT dan OT dalam praktik

  • Solusi yang ada secara umum terbagi menjadi CRDT dan OT
    • CRDT memberi setiap karakter ID tetap atau “position”, lalu mengurutkan ID dengan total order matematis seperti traversal tree khusus
    • OT mentransformasikan operasinya sendiri dengan mempertimbangkan penyuntingan serentak; dalam contoh ini, insert di index 17 diubah menjadi insert di index 22
  • Kedua pendekatan ini sudah dipakai di layanan nyata
    • Google Docs memakai OT
    • Pustaka CRDT Yjs digunakan di berbagai aplikasi
  • Bebannya muncul dari kompleksitas konseptual
    • Total order pada CRDT penyuntingan teks sering kali berupa algoritme subtil yang didefinisikan di makalah akademis
    • Algoritme OT harus memenuhi “properti transformasi” aljabar, jumlah kasusnya bertambah kuadratik, dan tanpa verifikasi formal sering rawan cacat
  • Algoritme yang rumit juga membuat implementasi menjadi rumit, sehingga biasanya orang memakai pustaka buatan ahli seperti kotak hitam jaringan
  • Ketika dibutuhkan fitur yang tidak diperkirakan pustaka, struktur monolitiknya bisa menjadi hambatan
    • Hanya memuat bagian dokumen besar yang diperlukan ke memori dan membiarkan sisanya di disk
    • Menegakkan izin subdokumen di server, seperti izin edit per paragraf atau izin penggunaan format tertentu
    • Menampilkan perubahan usulan ala Google Docs di dalam isi dokumen atau di sampingnya
    • Menyimpan teks dalam representasi yang mudah disinkronkan dengan key-value store seperti Replicache
    • Mendukung operasi selain insert dan delete, seperti memindahkan teks, memanipulasi tree dokumen, atau memisah dan menggabungkan paragraf

ID karakter dan pendekatan “insert after”

  • Ide dasarnya adalah memberi ID unik global pada setiap karakter alih-alih memakai indeks array
    • Struktur data intinya berbentuk Array<{ id: ID; char: string }>
    • Alih-alih insert di index 17, klien mengirim ke server operasi seperti insert " the" setelah f1bdb70a
    • Server mencari ID target lalu menaruh karakter baru tepat setelahnya
  • ID untuk karakter baru juga harus ditentukan oleh klien
    • Contoh: insert " the" setelah f1bdb70a dengan ids [...]
    • Jika klien menghasilkan ID sendiri, operasi insert after berikutnya dapat merujuk ID baru itu bahkan sebelum respons server diterima
  • Jika karakter yang dihapus benar-benar dibuang, posisi penyisipan bisa hilang
    • Saat Bob hendak menyisipkan setelah 26085702, pengguna lain mungkin menghapus karakter 26085702, sehingga server tidak tahu harus menyisipkan di mana
    • Server perlu mempertahankan ID yang sudah dihapus di daftar internalnya
  • Representasi status yang diperbaiki menjadi seperti berikut
Array<{ id: ID; char?: string; isDeleted: boolean }>
  • Teks yang terlihat oleh pengguna bisa dibuat dengan menggabungkan hanya item yang belum dihapus
list.filter(elt => !elt.isDeleted).map(elt => elt.char).join('')

Menangani insert dan delete

  • Saat karakter diketik, perilaku klien dan server sederhana
    • Klien mencari before, yaitu ID karakter tepat sebelum titik penyisipan
    • Klien menghasilkan id unik global seperti UUID untuk karakter baru
    • Klien mengirim ke server operasi untuk menyisipkan char dengan id setelah before
    • Server mencari before, termasuk di antara item yang sudah dihapus, lalu menyisipkan { id, char, isDeleted: false } tepat setelah item itu
  • Penghapusan karakter juga ditangani berbasis ID
    • Klien mencari id karakter yang akan dihapus
    • Klien mengirim ke server operasi untuk menghapus item dengan ID tersebut
    • Server mencari item itu dan, jika belum dihapus, mengatur entry.isDeleted = true
  • Dengan cara ini, masalah posisi operasi edit yang dikirim ke server dapat diselesaikan langsung tanpa mengikuti makalah CRDT atau OT
  • Implementasi array sederhana bisa tidak efisien karena harus menyimpan UUID per karakter; optimisasinya dibahas di Articulated

Pembaruan optimistis dan rekonsiliasi server

  • Dalam penyuntingan kolaboratif ala Google Docs, pengguna harus bisa langsung melihat hasil inputnya tanpa menunggu respons server
  • Bagian yang sulit adalah ketika di klien masih ada operasi lokal tertunda yang belum disetujui server, lalu dari server datang operasi jarak jauh yang terjadi bersamaan dengannya
  • Dalam kasus ini, CRDT tidak wajib; hal itu bisa ditangani dengan rekonsiliasi server (server reconciliation)
    1. Batalkan semua operasi lokal tertunda untuk memundurkan status klien ke sudut pandang status server sebelumnya
    2. Terapkan operasi jarak jauh agar klien selaras dengan status server
    3. Terapkan ulang operasi lokal yang belum diakui
  • Strategi yang lebih sederhana adalah Wait for Ack, yaitu melarang pemrosesan operasi jarak jauh selama masih ada operasi lokal tertunda
    • Klien Bob bisa mengabaikan pesan server pertama sampai menerima status server yang sudah memproses pesannya sendiri
    • Jika Bob terus mengetik atau latensi jaringan besar, penundaan ini bisa berlangsung tanpa batas sehingga kurang real-time dibanding rekonsiliasi server

Perbedaannya dengan CRDT

  • Pendekatan yang diusulkan berbagi beberapa ciri dengan CRDT karena memberi ID pada tiap karakter dan memakai penanda isDeleted
  • Bedanya ada pada cara menangani urutan
    • Dalam pendekatan ini, klien memberi tahu server untuk insert X setelah Y, lalu server menjalankannya begitu saja atau memprosesnya dengan cara lain yang didefinisikan pengembang
    • Dalam CRDT penyuntingan teks, ID diurutkan oleh algoritme yang kompleks
  • Hal utama yang membedakan berbagai CRDT penyuntingan teks satu sama lain juga merupakan algoritme pengurutan ID ini, dan pendekatan ini menghindari bagian tersebut

Hasil dari penyisipan serentak

  • Jika beberapa pengguna mengetik di lokasi yang sama secara bersamaan, hasilnya akan ditempatkan dalam urutan terbalik dari urutan server menerima operasinya
  • Misalnya teksnya "My name is", lalu Charlie mengetik " Charlie" dan Dave mengetik " Dave" secara bersamaan
    • Jika operasi Charlie tiba lebih dulu, server membentuk "My name is Charlie"
    • Karena operasi Dave juga menyisipkan setelah ID s pada is yang sama, hasilnya menjadi "My name is Dave Charlie"
  • Operasi insert after terhadap ID target yang sama akan menjadi urutan terbalik dari urutan penerimaan server bahkan ketika tidak ada konkurensi
  • Meski begitu, kata yang diketik dari kiri ke kanan tidak akan tercampur per karakter
    • Walaupun Dave mengirim tiap karakter sebagai operasi terpisah, a akan disisipkan setelah D, dan v setelah a
    • Status server berubah seperti "My name is D Charlie""My name is Da Charlie""My name is Dav Charlie""My name is Dave Charlie"
  • Jika pengetikan dilakukan dari kanan ke kiri, hasil teks juga bisa saling terselang-seling bila operasi Charlie dan Dave tiba ke server dalam urutan berselang-seling
    • Dalam praktik, ini bisa terjadi ketika dua pengguna sama-sama online pada saat yang sama dan mengabaikan edit yang sedang berlangsung dari satu sama lain

Server dapat mendefinisikan operasi yang lebih fleksibel

  • Dengan rekonsiliasi server, server pada dasarnya dapat memproses operasi klien dengan cara yang diinginkannya, dan klien pada akhirnya akan mencapai status yang sama
  • Ini kontras dengan CRDT dan OT yang hanya mengizinkan operasi yang memenuhi aturan aljabar ketat
  • Untuk penyisipan serentak di lokasi yang sama, server dapat merespons dengan berbagai cara
    • Mengabaikan operasi tersebut dan memperlakukannya sebagai no-op
    • Menambahkan ID ke daftar internal tetapi langsung menandainya sebagai terhapus agar operasi Dave berikutnya tetap bisa merujuk ID sebelumnya
    • Menyisipkan teks tetapi memberi kedua kata format khusus untuk peninjauan
    • Mengubah edit Dave menjadi “usulan” yang ditampilkan di samping isi dokumen
    • Menanyakan ke LLM bagaimana teks seharusnya diperbaiki
  • Klien juga dapat mengirim operasi yang lebih mencerminkan niat pengguna
    • insert before dapat dipakai saat membuat judul di atas paragraf agar judul tidak masuk ke tengah penyisipan serentak di akhir paragraf sebelumnya
    • Operasi fix typo dapat membawa syarat seperti sisipkan u setelah o pada color dengan ID X, tetapi hanya jika kata di sekitarnya masih color
  • Server juga dapat mendefinisikan operasi yang lokasi penyisipannya sendiri berubah setelah operasi tiba di server
    • Penyisipan serentak di lokasi yang sama bisa diurutkan ulang secara alfabetis
    • Jika ditambahkan operasi move untuk drag-and-drop, maka insert after di dalam teks yang dipindahkan dapat diterapkan ke dalam teks yang telah dipindahkan, bukan ke lokasi asalnya

Menangani format rich text

  • Dalam rich text, yang ditangani adalah format inline seperti tebal, ukuran huruf, dan hyperlink
  • Format rentang juga bisa dinyatakan dengan ID karakter alih-alih indeks
    • Contoh: terapkan bold dari ID X sampai ID Y
    • Jika didefinisikan sebagai dari ID X inclusive sampai ID Y exclusive, maka penyisipan serentak di akhir rentang juga bisa dibuat menjadi tebal
  • Jika digunakan bersama editor rich text seperti ProseMirror, server dapat mencari indeks array saat ini untuk ID X dan Y, lalu menginstruksikan status ProseMirror lokal untuk membuat rentang tersebut tebal
  • Setelah itu, ProseMirror dapat mempertahankan format tebal pada teks yang disisipkan di dalam rentang tersebut
    • Namun server juga bisa memprosesnya berbeda sesuai operasi insert seperti bold set to false
  • Untuk memahami semantik rich text kolaboratif, esai Peritext essay dapat dijadikan referensi

Varian terdistribusi dan kaitannya dengan CRDT

  • Sampai di sini diasumsikan ada server terpusat yang menentukan total order operasi berdasarkan urutan penerimaan server dan memperbarui status otoritatif
  • Jika tidak ada server terpusat, atau server bersifat opsional, total order akhir untuk operasi dapat diberikan secara terdistribusi
    • Misalnya, operasi diurutkan dengan Lamport timestamps
    • Setiap klien menganggap hasil pemrosesan berurutan atas semua operasi yang telah diterimanya sebagai status otoritatif
  • Dalam kasus ini, ID per karakter dan operasi insert after juga bekerja dalam rekonsiliasi terdistribusi “tanpa server”
  • Secara teknis, hasilnya memang menjadi CRDT penyuntingan teks
    • Karena ini adalah algoritme penyuntingan teks kolaboratif yang terdistribusi dan memiliki eventual consistency
  • Bergantung pada cara pengurutannya, pendekatan ini terhubung dengan CRDT yang sudah ada
    • Jika operasi diurutkan dengan Lamport timestamp, urutan daftar hasilnya setara dengan RGA / Causal Trees
    • Jika Lamport timestamp digabung dengan operasi format, perilakunya sangat mirip dengan Peritext
    • Jika digunakan topological sort depth-first, urutan daftar hasilnya setara dengan Fugue
  • Belum ada pembuktian rinci yang ditulis untuk klaim kesetaraan ini

Articulated: pustaka bantu implementasi

  • Dalam implementasi nyata, teks itu sendiri bisa disimpan di tempat lain seperti status ProseMirror, dan pendekatan ini mungkin hanya memerlukan daftar ID berbentuk berikut
Array<{ id: ID; isDeleted: boolean }>
  • Dari daftar ini, ada empat operasi yang sering dibutuhkan
    • Konversi antara ID dan indeks array saat ini
    • Menyisipkan ID baru setelah ID tertentu
    • Menandai ID sebagai terhapus
    • Menserialisasi dan memulihkan status untuk penyimpanan
  • Array sederhana tidak cocok untuk tugas-tugas ini
    • Operasi 1–3 memerlukan waktu linear
    • Karena menyimpan objek dan UUID untuk tiap karakter, kebutuhan memori dan ruang penyimpanannya besar
  • Articulated adalah pustaka npm kecil yang menyediakan fungsi setara dengan array ini
  • Struktur data intinya, IdList, memakai optimisasi yang mirip dengan pustaka CRDT penyuntingan teks populer
    • ID berbentuk { bunchId, counter }, dan bunchId adalah UUID yang dapat dibagi oleh banyak ID
    • Jika ID dari bunch yang sama bersebelahan, seperti pada kasus umum penyisipan dari kiri ke kanan, maka dalam memori dan status terserialisasi ia disimpan sebagai satu objek
    • Struktur data intinya bukan array melainkan B+Tree, sehingga waktu pemanggilan metodenya log atau log^2
  • IdList juga merupakan persistent data structure
    • Klien dapat menyimpan dengan murah status terakhir yang diterima dari server bersama status optimistis
    • Saat menerima operasi jarak jauh, mudah untuk rollback ke status server terakhir
  • Sebagai referensi tambahan, tersedia docs, demos awal, dan IdListSimple, implementasi sederhana kurang dari 300 SLOC
  • IdListSimple menghilangkan optimisasi dan persistensi, tetapi secara fungsional identik dan telah diverifikasi dengan fuzz tests

1 komentar

 
GN⁺ 2025-05-23
Pendapat Hacker News
  • Cukup rapi. Algoritmenya memberi setiap karakter teks ID unik global seperti UUID, sehingga bisa dirujuk secara konsisten dari waktu ke waktu alih-alih memakai indeks array yang terus berubah
    Klien mengirim operasi “insert after” yang merujuk ke ID yang sudah ada ke server, lalu server mencari ID target dan menyisipkan karakter baru tepat setelahnya. Penghapusan hanya disembunyikan secara tampilan, tetapi karakternya tetap disimpan untuk menghitung posisi “insert after”. Di luar penyuntingan teks pun tampaknya ada potensi, misalnya untuk sinkronisasi dunia game

    • Ini secara harfiah adalah CRDT yang terdegradasi. Cara server pusat menentukan urutan konflik sudah ada sejak era Google Wave
    • Rasanya ini tidak terlalu baru. Menggunakan proses pusat untuk menserialkan sistem terdistribusi hampir merupakan titik awal yang wajar, setidaknya sebelum mulai memikirkan partisi jaringan dan hal-hal seperti CAP. Sekarang juga muncul single point of failure. Saya hanya membaca sekilas dan jadi penasaran apakah ada pembahasan soal performa
    • Bukankah yang dijelaskan ini memang CRDT?
    • Kalau pakai ctrl+a, ctrl+x, ctrl+v, sepertinya tinggal berharap saja
  • Senang melihat tulisan seperti ini. Beberapa tahun lalu saya menemukan pendekatan yang sama, dan penasaran kenapa ini jarang terlihat di literatur akademik
    Hanya saja saya mengimplementasikannya sebagai CRDT dalam konteks terdesentralisasi agar sifat-sifat seperti komutatif, idempoten, dan asosiatif tetap terjaga

    • Kalau idenya adalah membuat alternatif dari CRDT, saya penasaran apa yang didapat dengan menjadikannya CRDT
  • Saya agak terkejut tidak ada pembahasan tentang struktur data lain seperti dict/map atau array bertipe arbitrer. Akan bagus kalau itu juga bisa diperluas dengan mudah. Dari pengalaman saya, aplikasi lebih sering membutuhkan struktur data kolaboratif daripada sekadar penyuntingan teks bersama murni
    Contoh motivasi seperti validasi pembaruan, pemuatan parsial, dan operasi tingkat tinggi memang menarik, tetapi kurang meyakinkan apakah alasan fitur-fitur seperti ini tidak ada di Yjs dan sejenisnya adalah karena implementasi CRDT dasarnya, atau memang sejak awal fitur-fitur itu sulit dibuat

    • Sepenuhnya setuju. Jika itu array dari objek “atomik” yang propertinya tidak bisa diubah, rasanya cukup dengan mengganti string menjadi tipenya sendiri. Perubahan di dalam objek akan lebih rumit, tetapi mungkin itu hanya masalah menyimpan dan menelusuri tree secara efisien
      Dalam istilah OP, saya selalu merasa pengguna library helper harus bisa menyisipkan logika model semantik yang ringan untuk mencegah atau mengelola state yang salah. Misalnya item tugas tidak boleh memiliki isDone: true dan sekaligus state: inProgress. Ini mirip dengan semantik pemformatan rich text yang disebut di tulisan yang ditautkan
    • CRDT pada dasarnya bekerja dengan memilih secara deterministik salah satu sisi saat konflik terjadi. Masalahnya, secara umum pendekatan ini tidak menjamin tidak ada kehilangan data maupun bahwa data tetap valid
      Bayangkan semua konflik merge Git diselesaikan dengan otomatis memilih salah satu sisi. Biasanya hasilnya akan salah, dan kadang bahkan menjadi kode yang tidak bisa dikompilasi. Jika tidak ada orang yang langsung memperbaikinya, hasilnya bisa jadi lebih membingungkan
      Karena itu saya rasa CRDT tidak menyebar lebih luas. CRDT hanya memperbaiki “masalah yang dikira ada”, dan tidak menyelesaikan masalah sebenarnya, yaitu resolusi konflik yang menjaga data, validitas, dan semantik. Bahkan bisa dibilang CRDT memperburuk masalah ini karena membatasi resolusi konflik hanya pada cara yang bisa direplikasi secara deterministik
  • Perbedaan kuncinya dari CRDT tampaknya adalah jika ada server pusat, maka sinkronisasi—yakni penetapan urutan di antara peristiwa yang bersamaan—sebaiknya dilakukan oleh server, bukan oleh struktur data itu sendiri lewat urutan leksikografis yang sudah ditentukan
    Karena semua komunikasi hanya terjadi antara klien dan server, bukan antarklien, saat klien terhubung ke server, server bisa menjamin bahwa semua operasi lokal klien itu diproses lebih dulu sebelum mengirim pembaruan jarak jauh yang baru

  • Jadi pesan utama tulisan ini adalah bahwa seluruh kompleksitas CRDT/OT hanya diperlukan jika tidak ada server pusat?

    • Bahkan tanpa server pusat pun, jika ada cara terdesentralisasi untuk pada akhirnya mengurutkan operasi ke dalam urutan total dan menerapkannya sesuai urutan itu, kompleksitas CRDT/OT bisa dihindari: https://mattweidner.com/2025/05/21/text-without-crdts.html#d...
      Seperti kata komentar lain, secara teknis ini juga CRDT dan merupakan bentuk yang cukup umum. Selain itu, implementasi yang membatalkan dan memutar ulang operasi juga tidak mudah. Meski begitu, semoga tetap lebih sederhana daripada memakai CRDT/OT tradisional untuk tiap tipe data
    • Itulah inti dari CRDT. Beberapa replika dari struktur data yang sama dikelola di banyak node, tiap replika diperbarui secara independen, dan pada akhirnya semuanya konvergen
    • OT membutuhkan server pusat
  • Saya bukan ahli di bidang ini, tetapi perbedaan utama dengan CRDT seperti Automerge tampaknya adalah koordinasi server. Misalnya, jika melihat tulisan ini [1], Automerge memakai nomor urut untuk menangani penyisipan serentak, dan jika penyisipan terjadi bersamaan, ia bergantung pada urutan ID agen yang telah disepakati. Sebaliknya, pendekatan ini bergantung pada server yang memproses berdasarkan urutan kedatangan
    Di tulisan itu ada kalimat seperti, “Dalam CRDT penyuntingan teks, algoritme canggih menentukan urutan ID. Algoritme pengurutan itulah yang membedakan berbagai CRDT penyuntingan teks, dan itulah bagian rumit dalam makalah CRDT. Kami menghindarinya sepenuhnya.” Karena banyak aplikasi memang sudah punya server pusat, masuk akal jika “algoritme canggih” itu bisa dihindari. Hanya saja, koordinasi server memerlukan pembatalan dan pemutaran ulang edit lokal, jadi saya tidak 100% yakin itu jauh lebih sederhana. [1] https://josephg.com/blog/crdts-go-brrr/

    • Saya setuju bahwa membatalkan dan memutar ulang juga sama sekali tidak sederhana. B+Tree persisten juga bukan benda yang sederhana
    • Sepemahaman saya, Automerge secara internal pada akhirnya menyimpan semua operasi dalam urutan total yang konsisten, dan itu bisa dipakai sebagai pengganti server dalam koordinasi server: https://mattweidner.com/2025/05/21/text-without-crdts.html#d...
      Namun Automerge sebenarnya tidak melakukan itu, dan menangani operasi teks dengan RGA, yaitu CRDT tradisional. Mungkin seperti yang Anda tunjukkan, karena implementasi pembatalan dan pemutaran ulang operasi memang tidak mudah
  • Jadi ini semacam CRDT yang tidak dioptimalkan? Seperti menetapkan ukuran himpunan maksimum ke 1 lalu langsung dijalankan?

    • Ada daya tariknya karena tampak seperti semacam kompleksitas yang tidak bisa direduksi. Lebih dekat dengan yang benar-benar terjadi dan sederhana. Seperti yang Anda bilang, memang belum dioptimalkan
  • Karena memakai koordinasi server, sepertinya koordinasi di sisi klien akan sulit. Bagaimana menjaga UX editor tetap mulus sambil menerapkan pembaruan server setiap kali datang?
    Misalnya, jika permintaan penyisipan karakter dari klien gagal, apakah cukup dicoba lagi? Bagaimana jika di sela-sela itu pembaruan sudah datang? Edit: bagian “Client-Side” mengakui kasus ini, menyarankan rewind lalu replay, dan juga mengusulkan opsi yang lebih sederhana yaitu memblokir sampai antrean tunggu kosong. Dari sudut pandang frontend, pengecualian UI/UX yang tidak dijelaskan bisa terus bercabang panjang, jadi secara keseluruhan CRDT mungkin justru lebih sederhana. Dan saya juga penasaran bagaimana rasa mengeditnya di kereta bawah tanah New York yang koneksinya mudah putus