Remote transaction overrides DOM selection

We’re using ProseMirror with Y.js for collaborative editing. In a somewhat extreme case which unfortunately happens often, a remote transaction (one coming from another connected client via Y.js) will override a user’s cursor position after they have clicked. What this looks like in practice is user A will click to change their cursor position while user B edits the document. User A sees their cursor move to the new position under their mouse, but then it bounces back to its previous position. With a lot of concurrent edits, it’s near impossible to move the cursor.

I’ve tracked down when this happens, but I’m unsure if it’s a bug in ProseMirror or if there’s something we can do about it on our end. The sequence of events for user A is as follows:

  1. mousedown event fires, indicating to the browser to change the selection in the DOM
  2. browser changes the selection in the DOM
  3. remote transaction from user B comes in, changes the doc
  4. because the doc has changed, ProseMirror modifies the DOM selection to match the changed state (with the old selection) using selectionToDOM
  5. selectionchange event fires, because browser has changed the selection
  6. DOMObserver.onSelection fires in response to the selectionchange event, but the selection in the DOM matches the previous selection (modified in step 4), so nothing happens

I realize it’s quite an edge case to have a transaction change the document between the mousedown trigger and the selectionchange event. It happens reliably with CPU throttling and often enough that it’s noticeable. I’d appreciate guidance on how we can fix it.

That’s odd. I was under the impression that the browser wouldn’t schedule things like asynchronous HTTP responses between a mousedown and the corresponding selectionchange event. What exactly is triggering the remote update in this situation?

The remote transactions are coming in via the y-prosemirror plugin through a websocket. I’m not sure where websocket messages fit into the browser’s event loop, but they can apparently come in between the mousedown and selectionchange events.

Looking at the spec, it seems that DOM events and other asynchronous events are indeed scheduled in the same way, and I was able to reproduce timeouts being called between mousedown events and their corresponding selectionchange.

Unfortunately, with ProseMirror’s existing interface, it is very hard to address the thing you’re seeing. When dispatch is called, the given transaction is already anchored in the specific state of the view at that moment, so we cannot, even if we were to query for pending DOM selection changes, update that state without invalidating the given transaction. We also (due to a blunder in the initial design) don’t have the precise transactions that were dispatched, on the view side, which would enable us to map the changed selection forward and apply it after the one passed to dispatch (though that could also invalidate client expectations in some situations, so I’m not sure it’d be a good idea to begin with).

You mention it can be near impossible to move the cursor. This suggests you must have changes come in at an alarmingly fast rate, which sounds like it would, even aside from this issue, be rather wasteful. At a glance, it sounds like throttling remote changes and only applying them every N milliseconds, or even just delaying them for a moment when pointer events happen, should help a lot with this problem.

1 Like