CRDT-inspired adaptation of prosemirror-collab

Hello all,

I’ve been working on a “CRDT-inspired” adaptation of prosemirror-collab, which you can find here, with the interesting code in prosemirror_wrapper.ts. It is just a prototype for now, but I wanted to share it and would be happy to hear any feedback.

The architecture is almost the same as prosemirror-collab, with a central server that totally orders all changes to the document. The difference is in how it rebases on top of concurrent changes: instead of mapping ProseMirror positions through the concurrent changes (OT-style), it annotates the original steps with immutable IDs (CRDT-style), then looks up where those IDs belong in the current document. The IDs (confusingly also called “Positions”) come from my list-positions library.

Relative to prosemirror-collab, this eliminates the need for clients to resubmit changes to the server: the server can append changes to the log as-is, and the IDs will still “make sense”, even if there were intervening concurrent changes.

Relative to y-prosemirror, the server’s authoritative log makes it easy to reject/modify changes on the server. Additionally, clients always apply remote changes using native ProseMirror steps, which can be useful for plugins/decorations (cf y-prosemirror issue 113).

I also found implementing this a lot easier than trying to figure out a “ProseMirror CRDT” that somehow makes all concurrent updates commute.

4 Likes

Interesting approach. I guess the server keeps some kind of map that stores these IDs for every ProseMirror-style document position?

Yes, the server and clients each store a tree describing the order on IDs, plus an “Outline” describing which IDs are currently present in the ProseMirror document.

For compactness, the tree groups sequential insertions by the same user into “bunches” of IDs, which are internally represented with a single object (instead of one object per ID). There’s more info in the list-positions internals.