3 điểm bởi GN⁺ 5 giờ trước | Chưa có bình luận nào. | Chia sẻ qua WhatsApp
  • Chỉ vài năm trước, đồng bộ dữ liệu multiplayer theo thời gian thực vẫn là một trong những bài toán khó nhất, đòi hỏi nhân lực chuyên môn và mức đầu tư của doanh nghiệp; nhưng giờ đây, chỉ với một lần npm install, ngay cả dự án cá nhân cũng có thể triển khai UI multiplayer
  • Automerge là công cụ để xây dựng mô hình dữ liệu có tính local-first, an toàn cho multiplayer và hỗ trợ quản lý phiên bản; theo cách tương tự pattern useState của React, nó tự động xử lý lưu trữ dữ liệu, quản lý lịch sử, broadcast tới cộng tác viên và giải quyết xung đột mà UI không cần bận tâm
  • Trong trường hợp của Ducking, một trình biên tập âm thanh multiplayer chạy trên trình duyệt, điều cốt lõi là thiết kế mô hình dữ liệu sao cho ánh xạ tự nhiên sang các phép toán CRDT
  • Với những trường hợp như sắp xếp lại danh sách mà Automerge chưa đảm bảo được, cần tự triển khai các bất biến mạnh hơn ở tầng ứng dụng
  • Ý nghĩa quan trọng nhất là việc chỉnh sửa cộng tác thời gian thực — thứ từng là phép màu ở cấp độ công nghiệp — nay đã có thể tự do áp dụng cho cả những ứng dụng nhỏ phục vụ số ít người

Bối cảnh — dự án Ducking

  • Trong vài tháng qua, tác giả đã xây dựng Ducking, một trình biên tập âm thanh multiplayer chạy trên trình duyệt, cho podcast của người đồng hành
  • Thật kỳ lạ khi việc biên tập âm thanh vẫn bị mắc kẹt ở mô hình ứng dụng desktop một người dùng từ 20 năm trước và cách trao đổi file qua lại
    • Khi một người chỉnh clip, người khác có thể sửa transcript hoặc điều chỉnh EQ; cần một quy trình cộng tác kiểu Google Docs hay Figma
    • Đồng thời cũng cần các công cụ cộng tác hiện đại như bình luận, lịch sử và theo dõi thay đổi
  • Ở bài trước, tác giả đã nói về thiết kế UI độc đáo và mô hình bố cục âm thanh giúp trình biên tập đơn tốt hơn, nhưng điều thực sự mong muốn là một quy trình làm việc mang tính cộng tác hơn

Cách Automerge hoạt động

  • Mọi dữ liệu của Ducking, ngoại trừ audio blob, đều được lưu trong tài liệu Automerge
  • Pattern cốt lõi có dạng quen thuộc với lập trình viên React: lấy dữ liệu bằng hook để render, dispatch yêu cầu thay đổi bất đồng bộ, rồi khi dữ liệu đổi thì hook sẽ kích hoạt render lại
    • Ví dụ dùng hook useDocument: nhận tài liệu theo dạng const [doc, changeDoc] = useDocument<Episode>(docUrl), rồi khi giá trị input thay đổi thì cập nhật bằng changeDoc((d) => { d.title = e.target.value })
  • Các phép cập nhật dữ liệu nhìn có vẻ mệnh lệnh, nhưng không giống object hay array JS native
    • Chúng có ít method hơn, không mutate ngay lập tức, và tự chặn các thay đổi để chuyển chúng thành các mục changelist trong lịch sử tài liệu
  • Với các nhu cầu đơn giản, Automerge xử lý được phần lớn việc cần làm, nhưng nó không phải phép màu; các bất biến của nó không phải lúc nào cũng trùng với ý nghĩa mà ứng dụng mong muốn, nên thiết kế mô hình dữ liệu cẩn thận là rất quan trọng
    • Hầu hết hành động người dùng ở cấp độ ý nghĩa nên tương ứng với một phép toán đơn mà Automerge cung cấp
    • Các hành động riêng biệt của người dùng trên dữ liệu liên quan nên được giải quyết một cách tự nhiên theo góc nhìn bất biến của phép toán Automerge đó
    • Cần tách bạch rõ giữa dữ liệu chuẩn được lưu trữ (canonical data) và dữ liệu suy ra từ tính toán (derived data)

Mô hình hóa dữ liệu cho multiplayer

  • Trong mô hình dữ liệu của Ducking, clip là một cửa sổ phát một phần của nguồn âm thanh nền bất biến, chịu trách nhiệm về khoảng phát, áp dụng hiệu ứng và chiếm không gian trên timeline
    • Hiệu ứng phổ biến nhất là clip điều chỉnh âm lượng của âm thanh nền theo thời gian để tạo crossfade hoặc loại bỏ tạp âm
  • Ban đầu, mỗi clip có một danh sách mức âm lượng được đánh chỉ mục theo thời gian tính từ đầu clip, nhưng điều này gây vấn đề vì phần lớn thay đổi âm lượng thực ra gắn với âm thanh nền chứ không phải clip
    • Nếu đẩy thời điểm bắt đầu clip sớm hơn một chút, toàn bộ thay đổi âm lượng sẽ áp vào phần khác của âm thanh
    • Việc viết code cập nhật mọi timestamp âm lượng mỗi khi thời điểm bắt đầu clip đổi là một lựa chọn tồi
  • Khi hai cộng tác viên cùng lúc chỉnh thời điểm bắt đầu clip, mỗi chỉnh sửa sẽ gộp cả thay đổi thời điểm bắt đầu lẫn toàn bộ timestamp tự động hóa âm lượng
    • Automerge không biết quan hệ nhân quả (causal relationship) giữa các thay đổi đó, nên khi merge chúng có thể bị giải quyết lộn xộn
    • Đây là vấn đề điển hình khi một hành động mang một đơn vị ý nghĩa lại cố cập nhật nhiều dữ liệu bền vững theo cách nhân quả mà CRDT không hiểu
  • Giải pháp là di chuyển dữ liệu hiệu ứng âm thanh khỏi clip, sang mốc thời gian của âm thanh nền
    • Khi đổi thời điểm bắt đầu hay độ dài clip thì không còn cần cập nhật gì nữa, nên nhiều người chỉnh thời điểm bắt đầu, tự động hóa âm lượng hay hiệu ứng khác cũng có khả năng được merge đúng cao hơn vì chúng độc lập với nhau
  • Khác biệt giữa UI một người dùng và UI multiplayer
    • Với UI một người dùng, đôi khi vẫn có thể giữ mô hình dữ liệu cũ rồi thêm tính toán ở thời điểm ghi
    • Với UI multiplayer, việc chuyển đổi mô hình dữ liệu để giữ mọi dữ liệu bền vững ở trạng thái trực giao (orthogonal) diễn ra thường xuyên hơn nhiều
  • Tác giả dần ưu tiên mạnh việc đơn giản hóa lúc ghi và tính toán lúc đọc, để tận dụng tối đa khả năng tự động merge của Automerge
  • Lời khuyên về migration hình dạng dữ liệu
    • Hãy chấp nhận rằng trong quá trình xây dựng sẽ phải migration hình dạng dữ liệu, và nên dành thời gian luyện tập từ sớm để bớt sợ lần migration lớn đầu tiên
    • Có nhiều pattern khác nhau như xử lý lúc đọc ở client hay nâng cấp hàng loạt ở server
    • Nếu tìm được một bất biến tiện lợi để kiểm tra trước và sau migration có giống nhau hay không thì công việc sẽ dễ hơn rất nhiều
    • Ở Ducking, tác giả export âm thanh của mọi dự án trước và sau migration rồi dùng audio fingerprint để kiểm tra có thay đổi hay không, nhờ vậy có thể tự tin triển khai cả những thay đổi schema lớn

Triển khai sắp xếp lại danh sách

  • Đôi khi, để có những đảm bảo mà Automerge không cung cấp, cần tự viết các bất biến mạnh hơn bằng code ở tầng ứng dụng
  • Vấn đề xuất hiện khi triển khai magnetic timeline của Ducking, tức danh sách đã sắp xếp của các clip sẽ được phát
    • Automerge có phép toán mảng để xóa hoặc chèn phần tử theo chỉ mục, nhưng không có phép sắp xếp lại nguyên tử (atomic re-order) cho phần tử đã tồn tại
  • Đã có những hướng giải quyết được biết tới
    • Martin Kleppmann đã công bố bài báo về phép toán sắp xếp lại danh sách nguyên tử
    • Cùng với Liangrun Da, ông cũng công bố bài "Extending JSON CRDTs with Move Operations"
    • Thậm chí còn có một draft PR để thêm tính năng này vào Automerge, nhưng vẫn chưa được merge
  • Vấn đề của cách sắp xếp lại đơn giản
    • Xóa object khỏi chỉ mục hiện tại rồi thêm lại vào chỉ mục đích
    • Dù kết hợp bất biến của hai phép toán này, vẫn không thể đảm bảo bất biến mong muốn rằng "khi có nhiều lần sắp xếp lại đồng thời, object chỉ xuất hiện đúng một lần trong danh sách"
    • Nếu có nhiều thao tác xóa và thêm đồng thời, object có thể xuất hiện ở nhiều vị trí trong danh sách (nếu Alice và Bob đều di chuyển B bằng delete+insert, hai thao tác xóa sẽ hợp nhất thành một tombstone, nhưng hai thao tác chèn lại tạo ra hai phần tử mới nên cả hai đều còn sống, dẫn tới B xuất hiện hai lần)
  • Cách tự triển khai bất biến "đúng một lần" ở tầng ứng dụng
    • Khi clip được chèn vào timeline, nó được gán một semantic id
    • Khi sắp xếp lại, hệ thống kích hoạt các phép xóa và chèn như trên
    • Ở thời điểm đọc, ứng dụng quét các mục trùng semantic id, tùy ý chọn phần tử đầu tiên chưa bị xóa và bỏ qua phần còn lại
    • Nhờ vậy, object chỉ tồn tại một lần trong danh sách và mọi người đọc đều luôn hội tụ về cùng một trạng thái cuối
  • Sắp xếp lại danh sách là phép toán duy nhất trong Ducking mà Automerge chưa cung cấp; nếu PR được merge thì logic ở tầng ứng dụng này sẽ không còn cần thiết

Lịch sử tài liệu (Document history)

  • Một UI multiplayer tốt cần công cụ quản lý lịch sử; cộng tác viên muốn xem những thay đổi diễn ra khi họ vắng mặt, bình luận theo diff, so sánh phiên bản cũ và rollback
  • Automerge theo dõi lịch sử phiên bản tài liệu và cung cấp các primitive rất tốt để xử lý lịch sử cũng như so sánh
    • Tuy nhiên, ứng dụng sẽ phải tự quyết định cách phơi bày thông tin đó và những khái niệm nào nên đưa tới người dùng
  • Tác giả khuyến nghị Patchwork lab notes của Ink & Switch
    • Đặc biệt thú vị là cách họ đưa branch ra cho người dùng và cách tiếp cận universal comments
  • Ducking hiện dùng một mô hình cộng tác và lịch sử tương đối đơn giản
    • Lịch sử phiên bản tuyến tính với các checkpoint có tên do người dùng đặt; checkpoint vừa là đơn vị nhóm các thay đổi, vừa là đơn vị để thảo luận, xem diff và rollback
    • chuỗi bình luận (comment thread) có thể gắn với một điểm cụ thể trong audio, một vùng trong transcript hoặc một checkpoint phiên bản
  • Tác giả cho biết hiện chưa có đủ lý do để đưa branch vào, nhưng về sau nó có thể hữu ích

Văn bản và marks

  • Làm việc với rich text là một vấn đề đặc biệt khó khi muốn đặt logic tùy biến lên trên văn bản có thể chỉnh sửa
    • Tác giả khuyến nghị bài báo Peritext để hiểu các khó khăn của rich text và phần mềm multiplayer nói chung
  • Schema rich text của Automerge có marks, tức các chú thích áp dụng cho phạm vi văn bản và vẫn giữ được tính nhất quán trong lúc văn bản bị chỉnh sửa
    • Chúng thường dùng nhất cho định dạng như in đậm hay in nghiêng, nhưng cũng có thể tạo mark tùy chỉnh riêng cho ứng dụng
  • Hai cách Ducking dùng mark tùy chỉnh
    • Theo dõi vùng transcript là đối tượng của một chuỗi bình luận
    • Theo dõi timestamp của các từ trong transcript, đồng thời vẫn cho phép chỉnh sửa
      • Dịch vụ phiên âm lưu transcript vào Automerge dưới dạng đối tượng richtext, trong đó từng từ được gắn mark chứa thông tin thời gian
      • Nếu chỉ sửa một lỗi gõ nhỏ ở một từ, mark vẫn được giữ nguyên nên toàn bộ thông tin thời gian được bảo toàn
      • Nếu sửa cả câu, một số mark ở giữa có thể bị mất, nhưng mark ở đầu và cuối câu vẫn còn nên ít nhất vẫn giữ được thông tin thời gian gần đúng
  • Một hạn chế của marks là datum phải là giá trị đơn giản, thường là chuỗi, và bản thân nó không được merge theo multiplayer
    • Với dữ liệu nhỏ và bất biến như thông tin thời gian transcript, có thể serialize JSON thành chuỗi
    • Với dữ liệu phức tạp hoặc có thể thay đổi như chuỗi bình luận, mark chỉ nên lưu id, còn dữ liệu thật lưu ở chỗ khác trong tài liệu
  • marks cung cấp một nền tảng rất tốt để xây dựng các tính năng ứng dụng trên rich text multiplayer

Bài tiếp theo — cấu trúc loạt bài

  • Bài này là phần 2 trong loạt 3 bài về quá trình xây dựng Ducking
    • Phần 1: giải thích thiết kế UI độc đáo của phần mềm
    • Phần 2 (bài này): khuyến nghị xem xét Automerge và cho thấy khả năng xây dựng dự án multiplayer như một thú vui cá nhân
    • Phần 3 cuối cùng dự kiến: nhìn lại trải nghiệm xây dựng Ducking
  • Một vài nhắc tới về phần 3 cuối
    • Tác giả dùng hỗ trợ từ LLM không phải để tăng năng suất công việc, mà để có thêm thời gian phác thảo và nằm võng
    • Niềm vui của việc làm phần mềm narrowcast chỉ cần làm hài lòng một nhóm rất nhỏ người dùng

Câu hỏi được dự đoán trước

Còn dữ liệu audio thì sao?

  • Mọi dữ liệu multiplayer đều được lưu trong Automerge, nhưng audio blob nền không được đặt trong Automerge để có phát lại nhanh, nên cần xử lý riêng
  • Mục tiêu là để một cộng tác viên mới có thể bắt đầu nghe và chỉnh sửa trong vòng 4 giây sau khi tải trang, nhanh hơn mở ứng dụng desktop và nhanh hơn rất nhiều so với tải toàn bộ file dự án
    • Một tập dài 1 giờ có thể dựa trên khoảng 1 GB âm thanh, bao gồm 4 giờ thu âm chất lượng phòng thu cộng với hiệu ứng và nhạc nền
  • Công việc mà dịch vụ audio xử lý khi upload để có cold start nhanh
    • Sao lưu audio gốc
    • Phiên âm giọng nói để phục vụ chế độ xem transcript
    • Tạo waveform cho chế độ xem timeline
    • Nếu chỉ dùng 1 phút trong 40 phút ghi âm thì phần lớn client chỉ cần nhận một hoặc hai mảnh nhỏ, nên audio được cắt thành các cửa sổ ngắn
    • Các mảnh được transcode sang định dạng nén để có phiên bản lossy phát ngay được, trong khi audio chất lượng cao vẫn tải nền
  • Tầng dữ liệu UI quản lý việc tải phiên bản nhanh của dữ liệu cần ngay theo ý định người dùng, cùng với phiên bản chất lượng cao của toàn bộ audio thực sự được dùng
    • IndexedDB API của trình duyệt rất hữu ích cho cache nhiều tầng và lưu trữ content-addressable; dùng như một hệ thống tự động eviction thì khi cần sẽ có, không dùng thì tự biến mất
  • Sau khi hoàn tất toàn bộ xử lý đó và cache cục bộ, phần UI còn lại có thể giả định khả năng truy cập ngẫu nhiên nhanh vào audio và tập trung vào quy trình biên tập

Vì sao làm UI trình duyệt + server thay vì ứng dụng local-first?

  • Tác giả thích các ứng dụng local-first kiểu Obsidian hoạt động hoàn toàn không cần server, nhất là khi chúng có đường thoát đáng tin cậy trong lúc vẫn cung cấp trải nghiệm trả phí trên cloud
  • Ban đầu, tác giả bắt đầu với phương án ứng dụng Tauri có lưu trữ trên hệ thống file cục bộ và đồng bộ server tùy chọn
    • UI được xây theo giao diện dữ liệu sao cho dù là server hay app cục bộ đều có thể cung cấp được
    • Đây là một biện pháp an toàn để về sau không có khoản tài trợ nào có thể cám dỗ việc kiếm tiền từ app bằng cách lock-in người dùng
  • Sau đó, tác giả nhận ra đây không phải SaaS mà chỉ là thứ muốn dùng cùng người đồng hành và một vài người bạn
    • Không còn động cơ để xử lý sai lệch, chi phí vận hành lâu dài cũng thấp, nên quyết định làm theo cách dễ nhất
  • Khi đạt được cold start khoảng 3 giây, không ai còn muốn tốn thời gian tải và cài ứng dụng native nữa
  • Tác giả hy vọng các ứng dụng audio có thể nhảy thẳng từ thế giới desktop-only hiện nay sang thế giới local-first có tùy chọn đồng bộ, bỏ qua giai đoạn SaaS lock-in kéo dài 10–20 năm ở giữa

Automerge có an toàn và web-scale không? Có nên dùng cho startup không?

  • Tác giả vui vẻ trả lời rằng mình không biết; điều đó không phải từ chối, mà đơn giản là thật sự không biết
  • Khi mới đi làm, chỉnh sửa multiplayer thời gian thực không xung đột là phép màu; 10 năm trước đã có lời giải cho vài bài toán cụ thể, nhưng cần đội ngũ có tài trợ và chuyên môn ở nhiều lĩnh vực
    • Còn ngày nay, chỉ cần cài một dependency là đa phần có thể làm UI khá trực quan và cộng tác thời gian thực với bạn bè
  • Về bảo mật, hiện tại Ducking được bảo vệ bằng quyền truy cập mạng giới hạn và bước authorization khi tạo kết nối websocket tới máy chủ Automerge
    • Người dùng không thể phát hiện hoặc chỉnh sửa dự án mà họ không được mời vào
    • Việc quy kết người dùng cho các chỉnh sửa và bình luận mới chỉ an toàn một phần, và phụ thuộc vào giả định rằng bạn bè sẽ không làm điều xấu
    • Các quyền chi tiết như chỉ được bình luận chứ không được sửa, chỉ được sửa một phần dự án, hay kiểm soát khả năng phát hiện dự án đều cần công sức thiết kế cẩn thận
  • Keyhive mà Ink & Switch đang phát triển cung cấp mô hình kiểm soát truy cập dựa trên capability an toàn về mặt mật mã
    • Nó có thể giúp chia sẻ công khai ứng dụng Automerge cho cả người dùng không đáng tin dễ hơn, nhưng hiện vẫn chưa sẵn sàng

Automerge có tốt hơn không?

  • Một giải pháp khác trong lĩnh vực này là Yjs, nhưng tác giả không thể đánh giá thay người đọc xem cái nào phù hợp hơn
  • Lời khuyên không đổi
    • Hãy suy nghĩ thật sâu về bài toán, làm vài phép ước tính sơ bộ về giới hạn có thể gặp, thử tạo prototype với nhiều lựa chọn, và thành thật thừa nhận rằng có thể bài toán của mình không khó đến mức cần giải pháp mới nhất và phức tạp nhất
  • Với Ducking, thông qua prototype nhanh và đọc tài liệu, tác giả xác nhận rằng Automerge đủ trưởng thành và có hiệu năng đủ tốt cho trường hợp sử dụng này
  • Quan trọng hơn, tác giả bị thu hút bởi hệ sinh thái Ink & Switch về mặt thẩm mỹ
    • Automerge không chỉ là một engine đồng bộ và quản lý phiên bản đơn thuần, mà là một phần của tầm nhìn lớn hơn nhằm làm cho phần mềm trở nên an toàn hơn, cộng tác hơn, linh hoạt hơn, vui hơn và mang tính cá nhân hơn
    • Tác giả hy vọng Keyhive và các dự án tương tự sẽ thành công, để những phần mềm nhỏ nhưng kỳ diệu dành cho số ít người dùng có thể lan rộng hơn

Chưa có bình luận nào.

Chưa có bình luận nào.