Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Summary
With current implementation of realtime collaboration, there was an issue of self-stuttering. This PR suppresses such stuttering by signing issued change operations and locally discarding broadcast changes that a client optimistically anticipates will be overwritten.
Problem Description
Current implementation of realtime collaboration relies on strongly idempotent changes being broadcast by clients, temporally sorted by the server and re-broadcast to all clients (including the client who issued the change initially).
While this provides a simple and straight-forward solution for achieving eventual consistency, it has a "self-stuttering" side effect:
In this scenario, client A observes the state change as
I -> X -> X -> Y
. Since reapplying a change doesn't affect the state (idempotency), this is equivalent to observing the state sequenceI -> X -> Y
, which is correct.Client B however, observes the sequence
I -> Y -> X -> Y
, i.e. the state changing and then reverting again (stuttering). I call this self-stuttering because it is caused by them eagerly applying changes originated by themself (change Y). The desired sequence would beI -> Y
.This is particularly an issue as it can occur even without another client issuing changes on the same scope. Imagine the client issuing constant changes on the same scope (for example, by dragging an element). The following sequence of events is possible (and probable):
In this scenario, the client will observe the state sequence
I -> X -> Y -> X -> Y
, without interference from any other collaboration peer.Solution Details
The solution relies on each client optimistically assessing whether an incoming change from the server would be overridden by a following change in near future or not.
In case of current implementation, JSON Patch's replace operation is not only strongly idempotent, but also the main cause of self-stuttering. Its scope can be tracked by each client by tracking changes done to a certain
path
. To ensure the same change is broadcast back by the server, each client can sign each operation with a unique hash and check the hash of incoming operations.A
PatchVerifier
utility class has been added for this purpose:signOperation()
method checks if the operation is a replace operation, and in that case signs it with a unique hash and records itspath
andhash
pair in a mapping.sign()
signs all operations inside a patch.isVerifiedOperation()
checks whether some incoming operation is a signed replace operation or not. If it is, it checks whether itspath
is recorded in the aforementioned mapping. If not, then it verifies the operation (it can be applied without causing stuttering), otherwise it denies the operation (it can be optimistically discarded). If thehash
of a signed replace operation with a matchingpath
also matches, then the client has received its own issued change back from the server, and thepath, hash
pair is removed from the mapping (following changes on the path will be verified and applied).verified()
method clears up a patch, leaving only verified operations.