-
Notifications
You must be signed in to change notification settings - Fork 4.6k
Real-time collaboration: Add UndoManager support for collaborative editing #72407
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Size Change: +591 B (+0.02%) Total Size: 2.58 MB
ℹ️ View Unchanged
|
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
9ad513d to
8d92986
Compare
8d92986 to
f55808e
Compare
|
Flaky tests detected in 021c75d. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/19945808387
|
72a425a to
137489a
Compare
packages/core-data/src/entities.js
Outdated
| */ | ||
| supports: { | ||
| crdtPersistence: true, | ||
| undo: true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is undo an entity specific config (same question as awareness)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a fair question. We had originally introduced this back when the Undo Manager only worked on the post entity. It made sense considering we wanted undo/redo working only for that single entity.
We have since switched to use the multidocundomanager which allows for multiple entities to be supported, similar to the default Gutenberg undo manager albeit in a collaborative way.
Would you recommend that the default be to just have undo/redo support by default on all entities? Could there be entities where this may not be the case, or could that be ignored for now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say default on for all entities and we adapt later if needed. The main reason I see is that it would feel very weird for the user to be able to undo some changes but not others.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * CRDT documents (one per entity) and giving each peer their own undo/redo stack | ||
| * without conflicts. | ||
| */ | ||
| export function createUndoManager(): SyncUndoManager { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this deserves some unit tests, to check the behavior of undoing/redoing stuff. Seems important.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, it def is.
I'll be making a follow up PR with that. I wanted to limit the scope of this PR on just the UndoManager changes.
Happy to make it in this PR, if you'd prefer that.
| * | ||
| * @return The undo manager. | ||
| */ | ||
| export function getUndoManager( state: State ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One subtle issue here is that every-time state.undoManager changes, the getUndoManager private selector is called (if used within useSelect and things like that) which ensures the UI is always properly reactive.
But, it's not the case with the custom "sync" undo manager.
Solving that is a bit challenging. one possibility is building a custom @wordpress/data store in the sync package (a store is a function that returns subscribe, getActions, getSelectors) and ensure the listeners (folks that called subscribe) are actually called one new undo/redo actions are triggered. Once you have the store, you could transform getUndoManager to a "registry selector" (createRegistrySelector).
As evidenced by the above, it's a bit involved, so I wonder what you think. Should we try to address it or should we assume that if there's an undo or redo that is created something else in the "state" of core-data is probably changing at the same time (which triggers the selectors again). (Basically accept that the bug exists but is hidden, or hard to reproduce)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for pointing that out, and for explaining it so well.
Would you know off the top of your head, what could be a rough way to reproduce this bug? I'd be interested to see what the circumstances could be to trigger such a situation. We haven't been able to reproduce any issues in our testing, so maybe there is a workflow that we are missing. It'd be good to consider any such edge cases.
I'd lean towards accepting that the bug exists but is hidden or hard to reproduce. Given we have this as behind an experiment, it won't show up in the vanilla version of Gutenberg which is great. We can see if it comes up in during testing, and address it based on that.
we assume that if there's an undo or redo that is created something else in the "state" of core-data is probably changing at the same time
I think this is the assumption that I'd want to go with as well.
The multidocundomanager would keep track of all the changes in the editor, and so we'd be able to ensure that nothing is amiss.
Hopefully I clarified everything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd be interested to see what the circumstances could be to trigger such a situation. We haven't been able to reproduce any issues in our testing, so maybe there is a workflow that we are missing. It'd be good to consider any such edge cases.
Right now, it's not really possible to reproduce because any thing that creates an "undo" level also updates something else in the store (like entities data or something) which means the "subscribe" of the core-data store is triggered. But I'm not sure if this is a guarantee or just the current state of things.
Let's say tomorrow, we move "edits" out of the core-data store into the doc exclusively and the bug appears basically.
I'd lean towards accepting that the bug exists but is hidden or hard to reproduce. Given we have this as behind an experiment, it won't show up in the vanilla version of Gutenberg which is great. We can see if it comes up in during testing, and address it based on that.
I'm ok with this, I think we should mention the assumption above in the selector JSDoc to clarify that we're aware of it but we assume that an undo or redo level will always be accompanied by a change to the state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely understand that. I've updated the private selector with a summary of the issue, the assumption that's being made, and a link to this comment chain.
That should be sufficient for any changes that are needed down the line then.
What?
Add support for Yjs's undo manager, during a real-time collaboration session (RTC).
Why?
Currently, the undo manager shipped with Gutenberg is designed for the editor and the way it works right now. Each record in the undo/redo stack comes from actions within the editor.
It doesn't have support for RTC, using the way we use Yjs to make this possible. A Yjs doc has clients connected to it and have changes communicated amongst each other using providers such as webRTC, websockets, etc. So we need to use an Undo Manager that specializes in dealing with these providers.
This'll allow for:
How?
The default Yjs Undo Manager only supports a single ydoc (which maps to a single entity) at any given time. That's not the way Gutenberg works, as we need support for several entities. We would need to store several undo managers, and map them to the right entity and be able to decide which undo manager to use which isn't easy to do.
Instead, Yjs offers the YMultiDocUndoManager that resolves this. It's a modified version of the Yjs Undo Manager that tracks operations on a collection of Yjs docs. It's able to keep track of the order of operations, in the same vein as the undo/redo stack within Gutenberg and pick out the right order of entities to work on.
So when we expand RTC support beyond just post types, we wouldn't have to worry about the undo manager not working or not be scalable to work on several entities at the same time.
By adding this dependency within Gutenberg, we can avoid other plugins from needing to add this in their webpack. Instead, they can just use this out of the box.
Testing
Note: This provider uses requests to admin-ajax.php for signaling and the initial connection is delayed.
Testing Instructions for Keyboard
No UI changes in the PR.
Screenshare
Note: The screenshare was recorded using the real-time collaboration plugin, which provides a websocket server as a provider.
undomanager.mp4