How to go about building an alternative stateless undo implementation

I’m building a task management product which uses Y.js only for task titles (screenshot with dummy data below). Each task title has its own Y.Doc. Other fields (like priority and due date) use a different system.

I’m building out undo/redo now. The way it works is there’s a log of actions and for each action I generate an inverted action. For example if you update priority from Low to High then I’ll generate an inverted action that switches it back to Low. I’m hoping I can generate inverted actions for Y.js task titles as well.

I’ve spent some time looking at Y.UndoManager’s code. It’s calling a lot of internal functions so it’s not clear to me that it’ll be simple to reimplement in my code. So here’s what I have so far:

function getInvertedTitleUpdate(oldTitle, titleUpdate) {
    const yDoc = new Y.Doc();
    Y.applyUpdateV2(yDoc, oldTitle);

    const yUndoManager = new Y.UndoManager(yDoc.getXmlFragment("doc"));

    Y.applyUpdateV2(yDoc, titleUpdate);

    const invertedTitleUpdates = [];
    yDoc.on("updateV2", (invertedTitleUpdate) => {
        invertedTitleUpdates.push(invertedTitleUpdate);
    });

    yUndoManager.undo();

    yDoc.destroy();

    return Y.mergeUpdatesV2(invertedTitleUpdates);
}

This is working well for simple updates. But I’ve found at least one bug:

  1. Type “quick undo test”
  2. Replace “undo” with “redo”
  3. Now you have “quick redo test”
  4. Hit cmd-z, this gives me “quick undo test” (good)
  5. Hit cmd-z, this gives me “undo” (uh oh! expected an empty string “”)

I haven’t spent time trying to deeply understand the Y.js data format so I don’t understand what’s happening here. Some questions:

  • Is it possible to implement a getInvertedTitleUpdate(oldTitle, titleUpdate) function? What’s the implementation if so?
  • How does Y.UndoManager avoid the bug I’ve observed?