Is there a way to revert to specific version?

Hello,

I am working on a collaborative drawing application and using yjs websocket server.
If I can, I want to add version control function (like google-docs, revert to previous version).

I checked out ‘y-prosemirror-versions’ example(https://demos.yjs.dev/prosemirror-versions/prosemirror-versions.html), but it seems like it is usable for text editor.

Is it possible reverting to previous specific version using snapshot or undoManager?

I’m trying to overwrite y-docs with stored snapshot, but I’m not sure if it works or not.

Thanks and regards
Rory

Hi @rory,

The API for reverting to old document state is not yet public. y-prosemirror uses hidden methods to read the previous state using a state-vector. It is my intention to provide an easy-to-use API based on this approach this year.

For now, maybe you can find another way around it. You could, for example, store the versioned state as a JSON object and restore the previous state by updating the Yjs state to the previous version. For drawing applications, it is probably not a problem to simply replace the complete state.

2 Likes

I recently implemented this and took a “git revert” approach rather than a “git reset; git push --force” approach. I created a new temporary document from the snapshot, instantiated an Y.UndoManager to track changes, then brought the snapshot up-to-date with the current document, then undid all those changes using the UndoManager, then brought the current document up-to-date with the temporary one.

This approach has the advantage that you don’t need to convince all the connected clients to suddenly reload the document from the server, it’s just treated as a regular update.

4 Likes

@wmhilton,

I’m trying to do a “git revert” exactly as you described and am investigating using the exact same methodology. Any chance you should share a gist of what you came up with? I’d prefer not to have to re-invent the wheel, if at all possible.

Thanks!
Sam

Haha, due to popular demand (and personal emails asking me “how the heck do I do this?”) the code I used is:

revertChangesSinceSnapshot(snapshot: string) {
  // this removes the leading `\x` from the snapshot string that was specific to our implementation
  const buff = toUint8Array(snapshot.replace(/^\\x/, ''));
  const snap = Y.decodeSnapshot(buff);
  const tempdoc = Y.createDocFromSnapshot(this.yDoc, snap);

  // We cannot simply replace `this.yDoc` because we have to sync with other clients.
  // Replacing `this.yDoc` would be similar to doing `git reset --hard $SNAPSHOT && git push --force`.
  // Instead, we compute an "anti-operation" of all the changes made since that snapshot.
  // This ends up being similar to `git revert $SNAPSHOT..HEAD`.
  const currentStateVector = Y.encodeStateVector(this.yDoc);
  const snapshotStateVector = Y.encodeStateVector(tempdoc);

  // creating undo command encompassing all changes since taking snapshot
  const changesSinceSnapshotUpdate = Y.encodeStateAsUpdate(this.yDoc, snapshotStateVector);
  // In our specific implementation, everything we care about is in a single root Y.Map, which makes
  // it easy to track with a Y.UndoManager. To be honest, your mileage may vary if you don't know which root types need to be tracked
  const um = new Y.UndoManager(tempdoc.getMap(ROOT_YJS_MAP), { trackedOrigins: new Set([YJS_SNAPSHOT_ORIGIN]) });
  Y.applyUpdate(tempdoc, changesSinceSnapshotUpdate, YJS_SNAPSHOT_ORIGIN);
  um.undo();

  // applying undo command to this.ydoc
  const revertChangesSinceSnapshotUpdate = Y.encodeStateAsUpdate(tempdoc, currentStateVector);
  Y.applyUpdate(this.yDoc, revertChangesSinceSnapshotUpdate, YJS_SNAPSHOT_ORIGIN);
}

It won’t work exactly as-is because it was part of a class and some things like this.yDoc and YJS_SNAPSHOT_ORIGIN are defined outside the function, but you should be able to piece together how it works. I hope.

2 Likes