Multi User Collaborative Editing with ydonet
I’ve been working on a side project that features collaborative, live editing. Multiple users can each open the same document and work together on it. A user can even go offline and continue to make changes. When they return, they will sync with the other users and everyone’s local state will resolve to the same contents.
The Yjs project provides the core functionality. Yjs is an implementation of Conflict-Free Replicated Data Types (CRDTs). CRDTs are data structures where multiple nodes can make changes and as long as you have reliable messaging, each node will eventually converge to the same state when it has received all updates from its peers. This is deep computer science!
My design looks something like this:
On the client, I’m running the Yjs library to manage the state of the document. There’s a really
nice adapter for Vue called SyncedStore. This lets me keep all the
document state in a single javascript object backed by a Vue ref
. SyncedStore integrates with Vue’s
reactivity system so that whenever a property of that document changes, it will update the underlying
Yjs CRDT.
I also wrote a custom synchronization provider for Yjs that uses SignalR to exchange changes with the server and receive updates from other peers.
Yjs is a JavaScript library and I wanted to use dotnet on the server. Luckily, the Yjs ecosystem has a server side implementation built in rust called yrs (pronounced “wires”). Furthermore, there is a ydotnet nuget package that provides a dotnet interop layer so that developers can use it in C# projects.
There’s a challenge here though: The yrs
library isn’t thread safe. I couldn’t safely access it concurrently from multiple
incoming WebSocket messages from SignalR. I used Akka.net to force serialized access to the yrs
document. Every
document that’s open for editing is represented by a single Akka.net actor on the server. This actor takes care of receiving changes from clients,
applying them to yrs
, durably storing the document, and then broadcasting the changes out to all other clients. Once all
clients have finished editing and disconnect, the actor shuts itself down.
I wrote my own storage system for yrs
that appends all changes to a file on disk. When the actor starts up, it can
restore the current state of the yrs
document by playing back all those changes. Periodically, after about 100 updates, the
actor compacts the storage format and stores a snapshot as a backup. This improves boot up performance.
Y-dotnet
also let me subscribe to changes in the document. Whenever certain properties change (like the title) I update some metadata
stored in a PostgreSQL database. This way user’s can see a list of their documents without having to pull each one from disk and
inspect it.
I plan to write more about this project. I think local-first, offline-capable, collaborative editing is going to become an important differentiator for powerful web applications in the future. It’s great that there are so many excellent tools available that take the computer science research and make it accessible to developers.