Mapping pipelines and why they are necessary

Hi! I just read the blog post on collaborative editing. I am confused about the problem described in the “Rebasing Positions” section. I don’t fully understand the solution (“mapping pipelines”), but it sounds complicated. I would like to understand more clearly how it works but also why it is necessary. The problem that it solves is that a user could have multiple changes that need to be “rebased” on top of remote changes, and complications arise because the later changes will be against an already transformed state. Why can’t prosemirror just “squash” all the local changes into one (to continue with the git terminology) and then rebase that? The only drawback I can think of is that it might make undo slightly less fine-grained, but that seems really minor assuming the user has decent connectivity (and the “Offline Work” section of the blog post says that the whole approach being described is useless when the user does not).

Good question. ProseMirror changes are atomic changes, not diff-style collections of changes, and can, in general, not be combined together. So they do have to be rebased individually. Here’s an example:

The start document is empty.

The local user inserts ‘ac’ (change L1) and then inserts ‘b’ between ‘a’ and ‘c’ (change L2).

A remote user inserts ‘x’ (change R1), and that change goes through first, so the local user has to rebase L1 and L2 on top of that.

That involves transforming L1 through R1 (yielding L1*), and transforming L2 backward through L1, and then through R1 and L1*. The issue is that L1 covers L2 (L2 occurs in text inserted by L1), so transforming L2 through it loses information (the precise location of L2). But if we know that our pipeline (-L1, R1, L1*) contains an inverse of -L1 (namely L1*), so instead of going through the whole pipeline, we skip directly from -L1 to L1*, allowing us to move through the pipeline without losing information. For example, since L2 occurs one character after the start of -L1, so we map it to one character after the start of L1*, which is the appropriate location.

Thanks, but I’m still confused.

First, is the problem that there is not a natural composition operation on ProseMirror changes at all or that leveraging it would somehow lead to bad results? If there is no such operation at all then I am really confused.

Second, is it possible your example is too simple? Because I don’t see how it leads to a problem. Intuitively, the full sequence of changes should be:

epsilon -> 'x' -> 'xac' -> 'xabc'


epsilon -> 'x' -> 'acx' -> 'abcx'

If you precombine L1 and L2 to get

epsilon -> 'abc'

Then the resultant reduced sequence will be

epsilon -> 'x' -> 'xabc'


epsilon -> 'x' -> 'abcx'

I guess one possible issue is that in general a single-step ProseMirror change cannot be reconstructed from the start and endpoint because of the location mapping, so my notation for changes is too simple in general. But if that is the thing I need to understand, maybe the example is too simple since there is no ambiguity about what the location mapping could be, right?

If there is no such operation at all then I am really confused.

There isn’t. I’m not sure why that would be confusing. Changes are atomic things, and you can’t represent a change at the start of the document and one at the end as a single change, only as a sequence of changes.