Xây dựng Live Presence và Collaborative Editing với con trỏ thời gian thực
Hướng dẫn kiến trúc và triển khai Live Presence, multiplayer cursors và Collaborative Editing bằng Yjs CRDT, WebSocket, awareness, Redis, persistence và scale-out.
ShanverseCập nhật 13 phút đọc— lượt xemĐang tải lượt xem
Collaborative Editing
Real-time
WebSocket
Mục tiêu của hệ thống
Một tính năng collaborative editing hoàn chỉnh thường cần:
Hiển thị ai đang mở cùng document.
Avatar, tên và màu riêng cho từng người.
Con trỏ chuột hoặc caret/selection cập nhật gần thời gian thực.
Nhiều người sửa cùng lúc mà không ghi đè thay đổi của nhau.
Reconnect và đồng bộ phần dữ liệu bị lỡ.
Offline editing nếu sản phẩm yêu cầu local-first.
Phân quyền đọc/ghi trên từng document.
Lưu snapshot, lịch sử và khả năng phục hồi.
Scale qua nhiều WebSocket server node.
Không nên tuyên bố “giống hệt Figma/Notion” về kiến trúc nội bộ. Mục tiêu là tạo
Bình luận
trải nghiệm người dùng tương tự
bằng các pattern phổ biến: WebSocket, awareness, CRDT, persistence và distributed messaging.
Tách ba mặt phẳng dữ liệu
Document plane
Chứa nội dung thật: text, blocks, comments, shapes hoặc structured state. Dữ liệu này phải merge đúng, persist và có khả năng replay/restore.
Awareness plane
Chứa trạng thái ngắn hạn:
Presence không cần lưu lịch sử lâu dài. Khi client offline hoặc TTL hết hạn, state phải biến mất.
Control plane
Quản lý identity, quyền truy cập, document membership, rate limits, audit và feature flags. Awareness packet không được tự cấp quyền cho người dùng.
CRDT hay Operational Transformation?
Hai hướng phổ biến:
Operational Transformation (OT): biến đổi operation theo thứ tự và context; thường cần server điều phối chặt.
Conflict-Free Replicated Data Type (CRDT): mỗi replica áp dụng thay đổi cục bộ; các update hợp lệ có thể hội tụ khi được trao đổi, kể cả khi đến khác thứ tự.
CRDT không tự giải quyết mọi bài toán. Nó không thay thế:
Authentication/authorization.
Business constraints cần strong consistency.
Persistence, backup và schema migration.
Quota, moderation hoặc audit.
Product rules khi hai thay đổi cùng hợp lệ về toán học nhưng không hợp lệ về nghiệp vụ.
Yjs là lựa chọn thực tế cho web editor vì có shared types, editor bindings, awareness protocol và nhiều network provider. Automerge phù hợp với nhiều mô hình local-first khác. Hãy benchmark theo document size, update rate, offline duration và editor framework.
Yjs: document và awareness là hai lớp riêng
Client tối thiểu:
Trong editor thật, không bind Y.Text với <textarea> bằng cách replace toàn bộ string sau mỗi keystroke. Hãy dùng binding chính thức của editor như ProseMirror/Tiptap, Lexical, Slate, Monaco hoặc CodeMirror để giữ selection và transaction semantics.
Live Presence với Awareness
Yjs Awareness được thiết kế cho dữ liệu ngắn hạn như user info, cursor và selection. Nó không phải phần nội dung trong Y.Doc.
Lắng nghe thay đổi:
Đừng dùng clientId của CRDT làm identity nghiệp vụ. Nó đại diện cho một replica/session và có thể thay đổi khi reconnect. Authorization phải dựa trên token và user ID do server xác minh.
Con trỏ chuột realtime như canvas design tool
Không gửi mỗi pointermove trực tiếp. Trình duyệt có thể phát hàng trăm event mỗi giây.
Render con trỏ remote:
Trong canvas có pan/zoom, tọa độ phải nằm trong document coordinate system, không chỉ viewport. Gửi canvasX/canvasY hoặc normalized coordinates kèm transform/version; phía nhận tự chuyển sang màn hình hiện tại.
Text caret và selection không phải pixel cursor
Pixel coordinates sẽ hỏng khi người khác chèn text trước vị trí caret. Với collaborative text, selection nên được biểu diễn bằng vị trí tương đối gắn với CRDT, chẳng hạn Yjs RelativePosition.
Binary values cần được provider/serializer hỗ trợ. Nhiều editor binding đã xử lý việc này và render remote caret; nên dùng binding thay vì tự xây toàn bộ mapping DOM.
Màu và danh tính ổn định
Không chọn màu random mỗi lần render. Cùng một user nên có màu ổn định trong session hoặc document:
Cần đảm bảo contrast, dark mode và không chỉ dùng màu để phân biệt. Hiển thị thêm avatar/name label và hỗ trợ screen reader cho danh sách collaborator.
Presence lifecycle và TTL
“Online” không nên được suy ra từ một database boolean vĩnh viễn. Dùng lease có thời hạn:
Connection refresh lease theo heartbeat.
Disconnect bình thường xóa lease.
Node crash không cleanup được thì TTL tự hết hạn.
Một user có thể có nhiều tab/device.
User chỉ biến mất khi không còn connection lease hợp lệ.
Trạng thái idle có thể dựa trên activity timestamp, nhưng không đồng nghĩa offline.
Yjs Awareness provider đã quản lý vòng đời awareness ở mức protocol; Redis TTL vẫn hữu ích cho analytics, API query hoặc hệ thống nhiều service.
Authentication và Authorization
Không truyền access token dài hạn trong query string vì URL có thể xuất hiện trong log. Tốt hơn:
Client gọi HTTPS API xin collaboration token sống ngắn cho documentId.
Token chứa user ID, document ID, role, expiry và nonce.
WebSocket server xác minh token trước khi join room.
Server kiểm tra role trước khi chấp nhận document update.
Awareness payload được sanitize và giới hạn kích thước.
Không tin name, color, userId do client gửi. Server hoặc client sau xác thực phải dùng profile đã được xác minh.
Quyền thường gặp:
viewer: nhận document updates và awareness, không gửi edit update.
commenter: tạo comment nhưng không sửa nội dung chính.
editor: gửi CRDT updates.
owner/admin: quản lý ACL và history.
Persistence: đừng chỉ giữ Y.Doc trong RAM
Một demo y-websocket có thể hoạt động bằng memory, nhưng production cần persistence.
Các chiến lược:
Snapshot + incremental updates
Append binary update vào durable log.
Định kỳ merge thành snapshot.
Khi load: đọc snapshot mới nhất rồi replay update sau snapshot.
Compact để tránh log tăng vô hạn.
Database row cho mỗi document
Phù hợp document nhỏ và lưu snapshot nguyên khối, nhưng cần kiểm soát concurrent write, kích thước row và backup.
Object storage + metadata database
Phù hợp snapshot lớn; metadata DB giữ version, checksum, ACL và object key.
Không persist Awareness cursor update. Nếu ghi 20 cursor update/giây/user vào database, chi phí và lịch sử vô nghĩa sẽ tăng rất nhanh.
Autosave và trạng thái “Saved”
Trong local-first/CRDT, “Saved” có nhiều tầng:
Update đã áp dụng vào local document.
Update đã gửi tới provider.
Server đã nhận.
Durable storage đã commit.
Replica khác đã đồng bộ.
UI nên đặt tên chính xác: Offline, Syncing, Saved to device, Saved to server. Không hiển thị “Saved” chỉ vì WebSocket send() không throw.
Scale-out qua nhiều server node
Khi người cùng document nằm ở node khác, update và awareness phải được chuyển xuyên node.
Redis Pub/Sub phù hợp cho awareness/fan-out có thể mất tạm thời. Document update quan trọng cần durable log hoặc cơ chế resync; không xem Pub/Sub là storage.
Thiết kế channel tránh broadcast toàn cụm:
Ở scale lớn, không nhất thiết node nào cũng subscribe mọi document. Có thể dùng consistent hashing, document affinity hoặc sharded Pub/Sub, nhưng phải đo hotspot: một document 1.000 editor khác hoàn toàn 10.000 document có 2 editor.
Throttle, coalesce và backpressure
Awareness data là replaceable state. Nếu có năm cursor updates đang chờ, thường chỉ cần gửi update mới nhất.
Throttle cursor khoảng 20–30 Hz rồi đo UX thực tế.
Coalesce pointer events.
Không gửi khi cursor không thay đổi đáng kể.
Tạm giảm tần suất ở background tab.
Drop awareness packet cũ khi queue đầy.
Không drop document updates; persist/resync theo protocol.
Giới hạn bufferedAmount và disconnect slow consumer khi cần.
Tách queue durable và ephemeral để cursor burst không chặn document edits.
Conflict không chỉ là text
Với block editor hoặc design canvas, cần định nghĩa merge semantics cho:
Hai người di chuyển cùng một shape.
Một người xóa block trong khi người khác sửa block.
Thay đổi parent/child đồng thời.
Resize và rotate cùng lúc.
Comment bám vào range đã bị xóa.
Undo/redo chỉ áp dụng thay đổi local hay toàn cục.
CRDT library giải quyết convergence, nhưng product phải quyết định trải nghiệm hợp lý. Có thể cần soft lock, intent indicator hoặc “user X is editing this field” cho object nhạy cảm.
Undo/Redo trong collaboration
Không dùng snapshot cũ của toàn document để Undo vì sẽ xóa thay đổi người khác. Undo manager phải theo dõi origin/transaction của local user.
Với Yjs, dùng Y.UndoManager trên shared types và cấu hình tracked origins phù hợp. Kiểm thử khi:
User A gõ.
User B gõ ở nơi khác.
User A Undo.
Chỉ thay đổi của A bị hoàn tác theo semantics mong muốn.
Offline và reconnect
Khi offline:
Local CRDT vẫn nhận edit nếu sản phẩm cho phép.
Awareness của user khác trở nên stale và phải biến mất sau timeout.
Update local được queue/persist trên thiết bị.
Khi reconnect, sync protocol trao đổi phần update còn thiếu.
Authentication phải được làm mới nếu token hết hạn.
Sử dụng IndexedDB provider nếu cần reload tab mà vẫn giữ local document. Cần quota policy và cleanup cache cho document cũ.
Hiển thị collaborator đúng UX
Một UI tốt thường có:
Avatar stack và số người đang online.
Tooltip name/role.
Màu caret/selection ổn định.
Cursor label tự ẩn sau một khoảng không di chuyển.
Trạng thái editing, viewing, idle thay vì một boolean online duy nhất.
Chỉ render cursor đang nằm trong viewport.
Gom collaborator dư thành +N.
Chế độ giảm chuyển động theo prefers-reduced-motion.
Không hiển thị email hoặc thông tin nhạy cảm trong awareness cho tất cả client.
Observability
Theo dõi:
Active documents, connections và collaborators/document.
Document update bytes/second và awareness messages/second.
Sync latency, cursor latency và reconnect rate.
Snapshot size, update-log length và compaction duration.
Authorization failures và invalid update rate.
Redis publish latency, subscription count và reconnects.
WebSocket buffered bytes, slow consumers, CPU, RSS và event-loop lag.
Không gắn userId dạng high-cardinality vào metric label. Đưa identity vào structured logs/traces có kiểm soát.
Kịch bản kiểm thử bắt buộc
Hai user cùng gõ tại một vị trí.
Một user offline, sửa, rồi reconnect.
Hai user xóa/sửa cùng block.
Cursor khi canvas pan/zoom và viewport khác kích thước.
Tab sleep rồi quay lại.
Kill một collaboration node.
Redis restart/network partition.
Document có hàng nghìn block và lịch sử update dài.
100+ cursor cùng di chuyển.
Viewer cố gửi edit update.
Reconnect lặp lại không tạo duplicate presence.
Undo local không xóa thay đổi của người khác.
Anti-patterns
Lưu cursor vào database như document content.
Dùng pixel màn hình tuyệt đối cho canvas cursor.
Dùng index text tuyệt đối cho collaborative selection.
Broadcast pointer event không throttle.
Tin user identity trong awareness payload.
Dùng Redis Pub/Sub làm nguồn dữ liệu document duy nhất.
Lưu Y.Doc chỉ trong RAM.
Undo bằng cách restore toàn bộ snapshot cũ.
Một global room cho mọi document.
Không giới hạn payload hoặc document size.
Hiển thị Saved trước durable acknowledgment.
Cho rằng CRDT tự giải quyết authorization và business invariants.
Checklist production
Document state và awareness state được tách riêng.
Editor binding hỗ trợ CRDT selection đúng cách.
Cursor được throttle/coalesce và dùng coordinate system ổn định.
Presence có heartbeat/TTL và connection ID.
WebSocket handshake xác thực document-scoped token.
Mọi update đều kiểm tra quyền.
Awareness payload được sanitize và giới hạn.
Document updates được persist, snapshot và compact.
Có resume/resync sau reconnect.
Durable và ephemeral queues tách biệt.
Multi-node fan-out có Redis/backplane.
Pub/Sub không được xem là durable storage.
Undo chỉ hoàn tác local origin phù hợp.
Có metrics cho sync, cursor, storage và backpressure.
Đã test offline, conflict, node failure và hotspot document.
Kết luận
Một hệ thống collaborative đáng tin cậy không chỉ là “gửi text qua WebSocket”. Nó kết hợp:
CRDT/OT để xử lý concurrent edits.
Awareness cho online users, cursor và selection.
WebSocket cho đường truyền độ trễ thấp.
Persistence để không mất document.
Redis/backplane để scale qua nhiều node.
Auth, observability và backpressure để vận hành production.
Nguyên tắc quan trọng nhất: document update là dữ liệu bền vững; presence và cursor là trạng thái tạm thời có thể thay thế.
Hướng dẫn lập trình WebSocket không cần thư viện ngoài: Native WebSocket API trên Browser, WebSocket server Node.js thuần, handshake, frame parsing, ping/pong, Chat UI và bảo mật.