4 điểm bởi GN⁺ 2025-05-23 | 1 bình luận | Chia sẻ qua WhatsApp
  • Trong ứng dụng cộng tác dựa trên máy chủ trung tâm, nếu chỉnh sửa văn bản theo chỉ mục mảng, vị trí sẽ bị lệch khi có chỉnh sửa đồng thời; vì vậy gắn ID duy nhất toàn cục cho từng ký tự và cập nhật trạng thái máy chủ bằng cách “chèn sau một ID cụ thể”
  • CRDTOT được dùng trong các dịch vụ thực tế rất mạnh, nhưng các thuật toán thứ tự toàn phần hoặc quy tắc chuyển đổi thao tác khá phức tạp, khiến việc thay đổi cơ chế bên trong cho phù hợp với yêu cầu ứng dụng trở nên khó khăn
  • Cách tiếp cận được đề xuất là client và server cùng duy trì danh sách ID dạng Array<{ id: ID; char?: string; isDeleted: boolean }>; các ký tự đã xóa vẫn được giữ lại như tombstone để tham chiếu vị trí chèn về sau không bị hỏng
  • Cập nhật cục bộ lạc quan được xử lý bằng điều hòa với máy chủ (server reconciliation): khi nhận thao tác từ xa, hoàn tác các thao tác cục bộ đang chờ, rồi áp dụng lại thao tác từ xa và các thao tác cục bộ chưa được xác nhận theo thứ tự
  • Bài viết cũng bàn về thứ tự chèn đồng thời, định dạng rich text, biến thể phân tán và thư viện Articulated, cho phép server định nghĩa các thao tác linh hoạt vượt ra ngoài chèn/xóa theo ngữ nghĩa riêng của từng ứng dụng

Vì sao chỉnh sửa dựa trên chỉ mục bị lỗi khi chỉnh sửa đồng thời

  • Trong chỉnh sửa văn bản cộng tác, client gửi thao tác do người dùng nhập lên server, và server phải cập nhật trạng thái có thẩm quyền của mình
  • Nếu xem văn bản như mảng ký tự và gửi thao tác kiểu chèn " the" tại index 17, thì trước khi thao tác đến server, một người dùng khác có thể đã chèn nội dung khiến cùng chỉ mục đó trỏ tới vị trí khác
    • Ví dụ, nếu Alice chèn " gray" ở phía trước, index 17 của Bob không còn là vị trí ban đầu nữa
    • Server phải rebase thao tác của Bob thành index 22
  • Vấn đề cốt lõi là client nên gửi thao tác nào lên server, và server nên diễn giải nó ra sao để có thể cập nhật văn bản theo cách “rõ ràng là đúng”
  • Vấn đề rebase chỉ mục này không chỉ xuất hiện trong các ứng dụng cộng tác thời gian thực như Google Docs, mà còn có thể xuất hiện trong form web chèn mục danh sách hoặc trong ứng dụng cục bộ đơn luồng xử lý bình luận inline và lịch sử chỉnh sửa

Những điểm CRDT và OT gây gánh nặng trong thực tế

  • Các giải pháp hiện có chủ yếu chia thành CRDTOT
    • CRDT gán cho mỗi ký tự một ID bất biến hoặc “position”, rồi sắp xếp các ID bằng một thứ tự toàn phần toán học như phép duyệt cây đặc biệt
    • OT chuyển đổi chính thao tác có xét đến chỉnh sửa đồng thời; trong ví dụ, nó đổi chèn tại index 17 thành chèn tại index 22
  • Cả hai cách tiếp cận đều đã được dùng trong các dịch vụ thực tế
    • Google Docs dùng OT
    • Thư viện CRDT Yjs được sử dụng trong nhiều ứng dụng
  • Gánh nặng đến từ độ phức tạp về mặt khái niệm
    • Thứ tự toàn phần của CRDT chỉnh sửa văn bản thường là những thuật toán tinh vi được định nghĩa trong các bài báo học thuật
    • Thuật toán OT phải thỏa các “thuộc tính chuyển đổi” mang tính đại số; số trường hợp tăng theo bình phương và dễ phát sinh lỗi nếu không có kiểm chứng hình thức
  • Thuật toán phức tạp làm việc triển khai cũng phức tạp, và thường khiến ta dùng thư viện do chuyên gia tạo ra như một hộp đen mạng
  • Khi cần tính năng mà thư viện không dự liệu, cấu trúc nguyên khối sẽ trở thành trở ngại
    • Chỉ nạp vào bộ nhớ những phần cần thiết của tài liệu lớn, phần còn lại để trên đĩa
    • Buộc thực thi quyền ở server cho từng phần con của tài liệu, như quyền chỉnh sửa theo đoạn hoặc quyền dùng một định dạng cụ thể
    • Hiển thị thay đổi đề xuất kiểu Google Docs trong nội dung hoặc bên cạnh nội dung
    • Lưu văn bản dưới dạng biểu diễn dễ đồng bộ với kho key-value như Replicache
    • Hỗ trợ các thao tác ngoài chèn/xóa, như di chuyển văn bản, thao tác cây tài liệu, tách/gộp đoạn

ID ký tự và cách “insert after”

  • Ý tưởng cơ bản là thay vì chỉ mục mảng, gắn ID duy nhất toàn cục cho từng ký tự
    • Cấu trúc dữ liệu cốt lõi có dạng Array<{ id: ID; char: string }>
    • Thay vì chèn tại index 17, client gửi cho server thao tác kiểu chèn " the" sau f1bdb70a
    • Server tìm ID đích và đặt ký tự mới ngay sau nó
  • ID của các ký tự mới cũng phải do client chỉ định kèm theo
    • Ví dụ: chèn " the" sau f1bdb70a với ids [...]
    • Nếu client tạo ID, nó có thể tham chiếu ID mới trong thao tác insert after tiếp theo trước khi nhận phản hồi từ server
  • Nếu xóa hẳn ký tự đã bị xóa, có thể mất vị trí chèn
    • Trong lúc Bob muốn chèn sau 26085702, nếu người dùng khác xóa ký tự 26085702, server sẽ không biết phải chèn ở đâu
    • Server phải giữ cả ID đã bị xóa trong danh sách nội bộ
  • Biểu diễn trạng thái đã điều chỉnh như sau
Array<{ id: ID; char?: string; isDeleted: boolean }>
  • Văn bản hiển thị cho người dùng có thể được tạo bằng cách nối các mục chưa bị xóa
list.filter(elt => !elt.isDeleted).map(elt => elt.char).join('')

Xử lý chèn và xóa

  • Khi nhập ký tự, hành vi của client và server khá đơn giản
    • Client tìm before, tức ID của ký tự ngay trước điểm chèn
    • Tạo ID duy nhất toàn cục id như UUID cho ký tự mới
    • Gửi thao tác lên server yêu cầu chèn char với id sau before
    • Server tìm before, bao gồm cả các mục đã bị xóa, rồi chèn { id, char, isDeleted: false } ngay sau mục đó
  • Việc xóa ký tự cũng được xử lý dựa trên ID
    • Client tìm id của ký tự cần xóa
    • Gửi thao tác lên server yêu cầu xóa mục có ID đó
    • Server tìm mục tương ứng và nếu nó chưa bị xóa thì đặt entry.isDeleted = true
  • Cách này trực tiếp giải quyết vấn đề vị trí của thao tác chỉnh sửa gửi lên server mà không cần đi theo các bài báo CRDT hay OT
  • Triển khai mảng đơn giản có thể kém hiệu quả vì phải lưu UUID cho từng ký tự; phần tối ưu hóa được xử lý trong Articulated

Cập nhật lạc quan và điều hòa với máy chủ

  • Trong chỉnh sửa cộng tác kiểu Google Docs, người dùng cần thấy ngay kết quả nhập của mình mà không chờ phản hồi từ server
  • Điểm khó là khi client còn thao tác cục bộ đang chờ chưa được server xác nhận, đồng thời nhận từ server một thao tác từ xa xảy ra đồng thời với chúng
  • Trong trường hợp này không nhất thiết cần CRDT; có thể xử lý bằng điều hòa với máy chủ (server reconciliation)
    1. Hoàn tác tất cả thao tác cục bộ đang chờ để tua trạng thái client về góc nhìn của trạng thái server trước đó
    2. Áp dụng thao tác từ xa để đưa client khớp với trạng thái server
    3. Áp dụng lại các thao tác cục bộ chưa được xác nhận
  • Một chiến lược đơn giản hơn là Wait for Ack, tức cấm xử lý thao tác từ xa khi còn thao tác cục bộ đang chờ
    • Client của Bob có thể bỏ qua thông điệp đầu tiên từ server cho đến khi nhận được trạng thái server đã xử lý thông điệp của chính mình
    • Nếu Bob tiếp tục nhập hoặc độ trễ mạng lớn, độ trễ có thể kéo dài vô hạn, nên cách này kém thời gian thực hơn server reconciliation

Khác gì so với CRDT

  • Cách đề xuất chia sẻ một số đặc điểm với CRDT ở chỗ gắn ID cho từng ký tự và dùng dấu isDeleted
  • Khác biệt nằm ở cách xử lý thứ tự
    • Trong cách này, client bảo server chèn X sau Y, và server thực hiện đúng như vậy hoặc xử lý theo một cách khác do nhà phát triển định nghĩa
    • Trong CRDT chỉnh sửa văn bản, ID được sắp xếp bằng thuật toán phức tạp
  • Điểm cốt lõi tạo nên khác biệt giữa các CRDT chỉnh sửa văn bản cũng chính là thuật toán sắp xếp ID, và cách tiếp cận này tránh phần đó

Kết quả do chèn đồng thời tạo ra

  • Nếu nhiều người dùng nhập đồng thời tại cùng vị trí, kết quả sẽ được đặt theo thứ tự ngược với thứ tự server nhận thao tác
  • Ví dụ, giả sử văn bản là "My name is" và Charlie nhập " Charlie", Dave nhập " Dave" cùng lúc
    • Nếu thao tác của Charlie đến trước, server tạo "My name is Charlie"
    • Thao tác của Dave cũng chèn sau ID của chữ s trong cùng từ is, nên kết quả trở thành "My name is Dave Charlie"
  • Các thao tác insert after sau cùng một ID đích sẽ theo thứ tự ngược với thứ tự server nhận, kể cả khi không có tính đồng thời
  • Dù vậy, các từ được nhập từ trái sang phải sẽ không bị trộn lẫn ở cấp ký tự
    • Ngay cả khi Dave gửi từng ký tự dưới dạng thao tác riêng, a vẫn được chèn sau D, và v sau a
    • Trạng thái server thay đổi như "My name is D Charlie""My name is Da Charlie""My name is Dav Charlie""My name is Dave Charlie"
  • Trong trường hợp nhập từ phải sang trái, nếu thao tác của Charlie và Dave đến server theo thứ tự xen kẽ, văn bản kết quả cũng có thể bị xen kẽ
    • Trên thực tế, điều này có thể xảy ra khi hai người dùng cùng online đồng thời và bỏ qua các chỉnh sửa đang diễn ra của nhau

Server có thể định nghĩa thao tác linh hoạt hơn

  • Khi dùng server reconciliation, server về cơ bản có thể xử lý thao tác của client theo bất kỳ cách nào mình muốn, và cuối cùng client vẫn đạt tới cùng trạng thái
  • Điều này trái ngược với CRDT/OT, vốn chỉ cho phép các thao tác thỏa quy tắc đại số nghiêm ngặt
  • Với chèn đồng thời ở cùng vị trí, server có thể phản ứng theo nhiều cách
    • Bỏ qua thao tác đó và xử lý như no-op
    • Thêm ID vào danh sách nội bộ nhưng đánh dấu xóa ngay lập tức, để các thao tác sau đó của Dave vẫn có thể tham chiếu ID trước đó
    • Chèn văn bản nhưng áp dụng định dạng đặc biệt để hai từ được review
    • Chuyển chỉnh sửa của Dave thành “đề xuất” hiển thị bên cạnh nội dung chính
    • Hỏi LLM nên sửa văn bản thế nào
  • Client cũng có thể gửi thao tác thể hiện ý định người dùng tốt hơn
    • insert before có thể dùng khi tạo tiêu đề phía trên một đoạn, để tránh tiêu đề bị chèn vào giữa các nội dung chèn đồng thời ở cuối đoạn trước
    • Thao tác fix typo có thể chứa điều kiện như chèn u sau o trong color có ID X, nhưng chỉ khi từ xung quanh vẫn là color
  • Server cũng có thể định nghĩa các thao tác mà vị trí chèn tự thay đổi sau khi đến server
    • Có thể sắp xếp lại các chèn đồng thời ở cùng vị trí theo thứ tự bảng chữ cái
    • Nếu thêm thao tác move cho kéo-thả, các insert after bên trong văn bản đã di chuyển có thể được áp dụng bên trong phần văn bản đã di chuyển, thay vì tại vị trí ban đầu

Xử lý định dạng rich text

  • Với rich text, ta xử lý các định dạng inline như in đậm, cỡ chữ, hyperlink
  • Định dạng theo phạm vi cũng có thể được biểu diễn bằng ID ký tự thay vì chỉ mục
    • Ví dụ: áp dụng bold từ ID X đến ID Y
    • Nếu định nghĩa là từ ID X inclusive đến ID Y exclusive, các chèn đồng thời ở cuối phạm vi cũng có thể được in đậm
  • Khi dùng cùng trình soạn rich text như ProseMirror, server có thể tìm chỉ mục mảng hiện tại của ID X và Y, rồi chỉ thị trạng thái ProseMirror cục bộ áp dụng in đậm cho phạm vi tương ứng
  • ProseMirror sau đó có thể giữ định dạng in đậm cho văn bản được chèn vào trong phạm vi đó
    • Tuy nhiên, server cũng có thể xử lý khác tùy theo thao tác chèn như bold set to false
  • Để hiểu ngữ nghĩa của rich text cộng tác, có thể tham khảo bài luận Peritext

Biến thể phân tán và liên hệ với CRDT

  • Cho đến đây, giả định là server trung tâm quyết định thứ tự toàn phần của các thao tác theo thứ tự server nhận được và cập nhật trạng thái có thẩm quyền
  • Trong ứng dụng không có server trung tâm hoặc server chỉ là tùy chọn, có thể gán thứ tự toàn phần cuối cùng cho các thao tác theo cách phân tán
    • Ví dụ, sắp xếp thao tác bằng Lamport timestamps
    • Mỗi client xem kết quả xử lý tuần tự các thao tác đã nhận được cho đến thời điểm đó là trạng thái có thẩm quyền
  • Trong trường hợp này, ID theo ký tự và thao tác insert after cũng hoạt động với cơ chế điều hòa phân tán “không server”
  • Về mặt kỹ thuật, kết quả này trở thành một CRDT chỉnh sửa văn bản
    • Vì đó là thuật toán chỉnh sửa văn bản cộng tác phân tán, có tính nhất quán cuối cùng
  • Tùy cách chọn thứ tự, nó có liên hệ với các CRDT hiện có
    • Nếu sắp xếp thao tác bằng Lamport timestamp, thứ tự danh sách kết quả tương đương với RGA / Causal Trees
    • Nếu dùng Lamport timestamp cùng thao tác định dạng, hành vi khá giống Peritext
    • Nếu dùng sắp xếp topo theo chiều sâu trước, thứ tự danh sách kết quả tương đương với Fugue
  • Chưa có chứng minh chi tiết cho tuyên bố tương đương này

Articulated: thư viện hỗ trợ triển khai

  • Trong triển khai thực tế, có thể lưu chính văn bản ở nơi khác như trạng thái ProseMirror, còn cách tiếp cận này chỉ cần danh sách ID dạng sau
Array<{ id: ID; isDeleted: boolean }>
  • Có bốn thao tác thường cần với danh sách này
    • Chuyển đổi giữa ID và chỉ mục mảng hiện tại
    • Chèn ID mới sau ID đã chỉ định
    • Đánh dấu ID là đã xóa
    • Tuần tự hóa và khôi phục trạng thái để lưu trữ
  • Mảng đơn giản không phù hợp cho các thao tác này
    • Các thao tác 1–3 mất thời gian tuyến tính
    • Việc lưu một object và UUID cho từng ký tự tốn nhiều bộ nhớ và dung lượng lưu trữ
  • Articulated là một thư viện npm nhỏ cung cấp chức năng tương tự mảng này
  • Cấu trúc dữ liệu cốt lõi IdList dùng các tối ưu hóa tương tự thư viện CRDT chỉnh sửa văn bản phổ biến
    • ID có dạng { bunchId, counter }, trong đó bunchId là UUID có thể được nhiều ID chia sẻ
    • Khi các ID cùng bunch nằm cạnh nhau, như trường hợp phổ biến là chèn từ trái sang phải, chúng được lưu thành một object trong bộ nhớ và trạng thái tuần tự hóa
    • Cấu trúc dữ liệu cốt lõi không phải mảng mà là B+Tree, nên thời gian gọi phương thức là log hoặc log^2
  • IdList cũng là cấu trúc dữ liệu persistent
    • Client có thể lưu rẻ cả trạng thái nhận lần cuối từ server và trạng thái lạc quan
    • Dễ rollback về trạng thái server gần nhất khi nhận thao tác từ xa
  • Tài liệu bổ sung gồm docs, các demo ban đầu, và IdListSimple, một triển khai đơn giản dưới 300 SLOC
  • IdListSimple bỏ qua tối ưu hóa và tính persistent, nhưng tương đương về chức năng và đã được kiểm chứng bằng fuzz tests

1 bình luận

 
GN⁺ 2025-05-23
Các ý kiến trên Hacker News
  • Khá gọn gàng. Thuật toán này gắn cho mỗi ký tự văn bản một ID duy nhất toàn cục kiểu UUID, để có thể tham chiếu nhất quán theo thời gian thay vì dùng chỉ số mảng luôn thay đổi.
    Client gửi lên server các thao tác “insert after” tham chiếu đến ID hiện có, server tìm ID đích rồi chèn ký tự mới ngay sau đó. Xóa thì chỉ ẩn về mặt hiển thị, còn ký tự vẫn được giữ lại để tính vị trí “insert after”. Ngay cả ngoài chỉnh sửa văn bản, chẳng hạn như đồng bộ thế giới game, cách này có vẻ cũng có tiềm năng

    • Đây đúng nghĩa là một CRDT suy biến. Cách để server trung tâm quyết định thứ tự xung đột đã có từ thời Google Wave rồi
    • Tôi không chắc cái này thật sự mới đến vậy. Dùng một tiến trình trung tâm để tuần tự hóa hệ thống phân tán gần như là điểm khởi đầu hiển nhiên, cho đến khi bạn phải lo về phân vùng mạng và CAP. Giờ thì bạn cũng có một điểm lỗi đơn. Tôi chỉ đọc lướt, không rõ có bàn đến hiệu năng không
    • Thứ được mô tả chẳng phải là CRDT sao?
    • Bấm ctrl+a, ctrl+x, ctrl+v thì chắc phải cầu may thôi
  • Thấy bài kiểu này thật vui. Vài năm trước tôi đã phát hiện ra cùng phương pháp, và từng thắc mắc vì sao nó không xuất hiện nhiều trong tài liệu học thuật.
    Tuy vậy, tôi triển khai nó như một CRDT trong bối cảnh phi tập trung, sao cho vẫn giữ được các tính chất như giao hoán, lũy đẳng và kết hợp

    • Nếu ý tưởng là tạo ra một phương án thay thế cho CRDT, thì tôi tò mò việc biến nó thành CRDT đã mang lại gì
  • Tôi hơi ngạc nhiên vì không thấy nói đến các cấu trúc dữ liệu khác như dict/map hay mảng kiểu tùy ý. Sẽ tốt nếu những thứ đó cũng mở rộng dễ dàng. Theo kinh nghiệm của tôi, ứng dụng thường cần cấu trúc dữ liệu cộng tác hơn là chỉnh sửa văn bản cộng tác thuần túy.
    Các ví dụ về đồng bộ như xác thực cập nhật, tải từng phần, thao tác cấp cao khá thú vị, nhưng lập luận rằng Yjs và các thứ tương tự không có những tính năng này là do phần triển khai CRDT nền tảng, hay vì ngay từ đầu các tính năng này khó xây dựng, thì có vẻ chưa thật thuyết phục

    • Hoàn toàn đồng ý. Nếu là mảng các đối tượng “nguyên tử” không thể thay đổi thuộc tính, có lẽ chỉ cần đổi chuỗi thành kiểu riêng là được. Thay đổi bên trong đối tượng sẽ khó hơn, nhưng có thể đó chỉ là bài toán lưu trữ và duyệt cây một cách hiệu quả.
      Nói theo thuật ngữ của OP, tôi luôn nghĩ rằng người dùng thư viện helper nên có thể chèn một chút logic mô hình ngữ nghĩa nhẹ để ngăn hoặc quản lý trạng thái sai. Ví dụ một mục việc cần làm không thể vừa có isDone: true vừa có state: inProgress. Điều này tương tự ngữ nghĩa định dạng rich text được nói tới trong bài được liên kết
    • Về bản chất, CRDT hoạt động bằng cách chọn một phía một cách quyết định khi có xung đột. Vấn đề là cách này nói chung không bảo đảm không mất dữ liệu, cũng không bảo đảm dữ liệu hợp lệ.
      Cứ tưởng tượng mọi xung đột merge trong Git đều được giải quyết bằng cách tự động chọn một phía. Thường sẽ cho ra kết quả sai, đôi khi còn thành mã không biên dịch được. Nếu lúc đó không có ai sửa ngay, kết quả sẽ càng hỗn loạn hơn.
      Vì vậy tôi cho rằng CRDT chưa phổ biến hơn. CRDT chỉ sửa “vấn đề mà ta tưởng là có”, chứ không giải quyết vấn đề thực sự là giải quyết xung đột sao cho bảo toàn dữ liệu, tính hợp lệ và ngữ nghĩa. Thậm chí có thể nói nó làm vấn đề tệ hơn vì chỉ giới hạn cách giải quyết xung đột vào những phương pháp có thể sao chép một cách quyết định
  • Điểm khác biệt cốt lõi so với CRDT có vẻ là: nếu có server trung tâm, thì việc đồng bộ, tức xác định thứ tự giữa các sự kiện đồng thời, không nên để chính cấu trúc dữ liệu xử lý bằng thứ tự từ điển, mà để server làm.
    Vì mọi trao đổi chỉ diễn ra giữa client và server, không giữa các client với nhau, nên khi một client kết nối tới server, server có thể bảo đảm xử lý hết các thao tác local của client đó trước khi gửi các cập nhật remote mới cho client

  • Thông điệp chính của bài này có phải là toàn bộ độ phức tạp của CRDT/OT chỉ cần thiết khi không có server trung tâm không?

    • Ngay cả khi không có server trung tâm, nếu có một cách phi tập trung để cuối cùng sắp xếp các thao tác thành một thứ tự toàn phần và áp dụng theo thứ tự đó, thì có thể tránh độ phức tạp của CRDT/OT: https://mattweidner.com/2025/05/21/text-without-crdts.html#d...
      Như các bình luận khác nói, về mặt kỹ thuật đây cũng là CRDT, và là một dạng khá phổ biến. Ngoài ra, bản thân việc triển khai hoàn tác rồi phát lại thao tác cũng không hề dễ. Dù vậy, tôi hy vọng nó đơn giản hơn việc dùng CRDT/OT truyền thống cho từng kiểu dữ liệu
    • Đó chính là cốt lõi của CRDT. Nhiều bản sao của cùng một cấu trúc dữ liệu được duy trì trên nhiều node, mỗi bản sao được cập nhật độc lập, và cuối cùng tất cả hội tụ
    • OT cần server trung tâm
  • Tôi không phải chuyên gia trong lĩnh vực này, nhưng khác biệt chính so với CRDT như Automerge có vẻ là điều phối bởi server. Ví dụ, xem bài này [1], Automerge dùng số thứ tự khi xử lý chèn đồng thời, và nếu các lần chèn xảy ra cùng lúc thì dựa vào thứ tự ID tác nhân đã được thống nhất. Ngược lại, cách này dựa vào việc server xử lý theo thứ tự đến.
    Trong bài có đoạn: “Trong CRDT chỉnh sửa văn bản, một thuật toán tinh vi quyết định thứ tự ID. Thuật toán sắp thứ tự đó tạo nên khác biệt giữa nhiều CRDT chỉnh sửa văn bản, và là phần phức tạp trong các bài báo CRDT. Chúng tôi tránh hoàn toàn điều đó”. Vì nhiều ứng dụng dù sao cũng có server trung tâm, nói rằng có thể tránh “thuật toán tinh vi” là hợp lý. Tuy nhiên điều phối bởi server đòi hỏi hoàn tác và phát lại các chỉnh sửa local, nên tôi không chắc 100% rằng nó đơn giản hơn nhiều. [1] https://josephg.com/blog/crdts-go-brrr/

    • Tôi đồng ý rằng hoàn tác và phát lại cũng không hẳn đơn giản. B+Tree bền vững cũng chẳng phải thứ đơn giản
    • Theo tôi biết, Automerge nội bộ cuối cùng lưu mọi thao tác theo một thứ tự toàn phần nhất quán, và điều đó có thể được dùng như một phương án thay thế cho server trong điều phối bởi server: https://mattweidner.com/2025/05/21/text-without-crdts.html#d...
      Nhưng Automerge thực tế không làm vậy, mà xử lý thao tác văn bản bằng RGA, một CRDT truyền thống. Có lẽ, như bạn chỉ ra, là vì triển khai hoàn tác và phát lại thao tác không dễ
  • Vậy tức là CRDT chưa tối ưu hóa à? Kiểu đặt kích thước tập tối đa là 1 rồi cứ thế làm?

    • Nó hấp dẫn vì trông giống một dạng độ phức tạp không thể giản lược. Gần với những gì thật sự diễn ra và đơn giản. Như bạn nói, chắc là chưa tối ưu
  • Vì dùng điều phối bởi server, phía client có vẻ sẽ khó xử lý. Làm sao duy trì UX trình soạn thảo mượt mà trong khi cứ mỗi lần cập nhật từ server đến lại phải áp dụng nó?
    Ví dụ nếu yêu cầu chèn ký tự do client gửi bị thất bại thì sao, chỉ thử lại à? Nếu trong lúc đó có cập nhật đến thì sao? Sửa: phần “Client-Side” thừa nhận trường hợp này, đề xuất tua ngược rồi phát lại, và cũng đề xuất phương án đơn giản hơn là chặn cho đến khi hàng đợi chờ trống. Nhìn từ góc độ frontend, các ngoại lệ UI/UX không được nêu rõ có thể kéo theo một chuỗi dài vấn đề, nên tổng thể có thể CRDT lại đơn giản hơn. Và tôi cũng tò mò cảm giác chỉnh sửa sẽ thế nào trên tàu điện ngầm New York, nơi kết nối dễ bị gián đoạn

    • ProseMirror và CodeMirror hiện đại có lời giải khá thanh lịch cho vấn đề này. Chúng mô hình hóa mỗi thay đổi của tài liệu thành một step theo dõi chỉ số, chứ không phải định danh node/văn bản, rồi dùng một cấu trúc dữ liệu gọi là “position map” để ánh xạ các step đã được buffer sang vị trí mới trước khi áp dụng vào tài liệu.
      Thực tế nó hoạt động khá tốt. Chi tiết ở đây:
      https://marijnhaverbeke.nl/blog/collaborative-editing.html
      https://marijnhaverbeke.nl/blog/collaborative-editing-cm.htm...