ueberdosis / hocuspocus

The CRDT Yjs WebSocket backend for conflict-free real-time collaboration in your app.
https://tiptap.dev/docs/hocuspocus/introduction
MIT License
1.15k stars 109 forks source link

Conditionally filter transactions #241

Open arrocke opened 2 years ago

arrocke commented 2 years ago

The problem I am facing Our application at Pivot Interactives has functionality that would benefit from being able to conditionally block transactions. For example, we have a series of questions that can be submitted to the server for grading. Occasionally, we get race conditions on slow networks where two clients aren't aware that the other is using the last submission, so the second one generates an error and gets the ydoc into a bad state. It would be great if we could prevent the second transaction from even being applied and sent to other clients.

The solution I would like I have no idea what is feasible here, we are new to Yjs and Hocuspocus so this could be a ridiculous request. One idea is to filter transactions from clients in a hook of some sort before they are applied to the server copy of the ydoc. Another idea is to be able to modify the ydoc and merge those changes into the existing transaction before it is sent to the other clients.

Alternatives I have considered Of course we can always listen for updates and then correct the ydoc if changes are made from clients that are not allowed. I'm not sure this is ideal because it will send out two transactions to all clients. One for the bad update, and then one to correct.

Additional context The other issue here is that if transactions are ignored, we would need to be able to tell the original client that their transaction was not accepted. If there is a technical solution within Hocuspocus, I'm willing to contribute with maybe a little help.

philipaarseth commented 2 years ago

I'm looking for something similar, essentially a way to partially sync a yMap. You can filter the incoming traffic on the other connected clients, but what I'm looking for is to not have to broadcast those messages in the first place.

hanspagel commented 2 years ago

Would you mind to elaborate more on your use case, so I get a better understanding of the problem?

arrocke commented 2 years ago

Our ydoc has a YArray of YMaps that correspond to questions in an assignment. To simplify a bit, each question has the shape below. Since, these questions are automatically graded, we have a submission limit to prevent random guessing. Once you run out of submissions, you can no longer change your answer and we disable the UI in the browser.

{
  id: '1234asdf',
  isCorrect: true,
  submissionsUsed: 2,
  submissionsLimit: 4,
  response: 1
}

On the backend we have observers attached to these Y objects in order to execute the grading process and update the isCorrect flag and submissionsUsed on the ymap. That process only occurs when we see the response field is the one that changed in the ymap

Let me set up the events that can cause issues for us:

  1. There is one remaining submission on the question. Let's say submissionsUsed = 0 and submissionsLimit =
  2. Two users change the response field on the ymap at nearly the same time.
  3. The server receives the first transaction and relays it to all of the clients
  4. The observer on the server processes the first response by setting isCorrect and incrementing submissionsUsed.
  5. The server receives the second transaction and relays it to all of the clients
  6. The observer on the server processes the second response and throws an error because there are no submissions. It resets the response field to the value of the last submission.
  7. This sends another transaction to all of the clients with the reverted changes.

What you can see here is that because the server has no way to intervene to modify how a transaction updates the ydoc before that transaction is forwarded to other clients, it sends transactions to clients that have the ydoc in an incorrect state and then has to send an updated transaction to correct itself. This results in a blip in the UI when it shows the bad response, and then the last submission.

arrocke commented 2 years ago

One other scenario that might be helpful here: We also have functionality to lock a group of questions. The assignment is grouped into sections and you have to lock one section before continuing to the next. This prevents further changes to those answers. So our ydoc is also tracking what parts of the assignment have been locked.

This creates the same race condition, if you change a response to a question at the same time another user locks that section. If the section locking transaction arrives first, the second transaction with the dropped response will still be broadcast and then reverted in another transaction.