I recently implemented ProseMirror plugin for Yjs (an operation-based CRDT). Demo / src
I map ProseMirror block nodes to YXmlElement
(a shared data type that has a node name, attributes, and a list of children) and ProseMiror text nodes to YXmlText
(a shared text type that supports formatting attributes - a.k.a. marks).
When the ProseMirror state changes, I compute a diff and apply the changes to the Yjs document. When the Yjs document changes, I simply create a new state based on the Yjs document and replace the old one. I do reuse old ProseMirror nodes, and it seems that this is fairly efficient given the immutable nature of ProseMirror state.
Mapping a CRDT to a ProseMirror document is not always possible, because ProseMirror has a schema that the document needs to comply to. E.g. given a blockquote
that must have at least one child. If client1
deletes the first child, and client2
concurrently deletes the second child, you may end up with a blockquote
without children (well, that depends on the CRDT). In this case I simply delete the node that does not comply with the schema anymore. This is why I went for the above approach of recreating the state based on the Yjs model, because manipulating the state with transformations would involve a lot more overhead. The disadvantage of my approach is that it destroys all decorations. The next step would be to find a method that only applies the diff between the old and the new state.