automerge / automerge-classic

A JSON-like data structure (a CRDT) that can be modified concurrently by different users, and merged again automatically.
http://automerge.org/
MIT License
14.75k stars 466 forks source link

Inconsistency when applying changes #410

Closed benjaminfayaz closed 3 years ago

benjaminfayaz commented 3 years ago

Version used: 1.0.1-preview.2 and 1.0.1-preview.3

In my "real" app I am trying to sync an array on multiple clients. However I noticed that applying changes sometimes won't work. This only seems to be the issue with arrays. Objects work fine.

Reproducing this is not easy since it occurs very inconsistently.

I created this (hopefully helpful) demonstration of the issue: Edit stupefied-water-ocxgk To see the inconsistency you have to refresh the browser multiple times and the results will be different almost every time.

I am also curious why the two elements never get merged into the final array. This does sometimes work on my "real" app but I couldn't reproduce this in the sandbox.

I also tried wrapping various function calls in setTimeouts hoping that this would be an issue caused by the fact that the callback inside Automerge.change did not get called or something, but no success.

Here is a GIF of the codesandbox's results in action: automerge-applyChanges

DUG-nick commented 3 years ago

Hello Benjamin,

you can check for conflicts with the Automerge.getConflicts() method.

I forked your CodeSandbox here to show you the usage and output.

The reason - as far as I understand the documentation - is that certain operations in CRDTs are non-deterministic and Automerge can therefore only ever guarantee that eventually the objects will converge (e.g. once you apply the changes the other way as well).

Maybe you can provide a litte more in depth view into the specifics of what goes wrong in your "real" application.

Kind regards, Nick

ept commented 3 years ago

Hi @benjaminfayaz,

The reason that you're not getting both elements merged into the same array is that you are creating two separate cards arrays, one in doc1 and the other in doc2. Each array creation is treated as a separate object, even if it's under the same key. Issue #4 discusses this in more detail. We are planning to fix this since many others have also run into the same problem.

You need to ensure the array is created only in one document, like this:

let doc1 = Automerge.init(), doc2 = Automerge.init(), patch;
// Create the cards array once
doc1 = Automerge.change(doc1, doc => doc.cards = []);
// Sync the empty array to doc2
[doc2, patch] = Automerge.applyChanges(doc2, Automerge.getAllChanges(doc1));

// Now you can insert elements on both sides, and they will go into the same array
doc1 = Automerge.change(doc1, doc => doc.cards.push('DOC1'));
doc2 = Automerge.change(doc2, doc => doc.cards.push('DOC2'));
[doc2, patch] = Automerge.applyChanges(doc2, Automerge.getAllChanges(doc1));
// doc2 should now contain both DOC1 and DOC2 (in either order)

The reason you're sometimes seeing ["DOC1"] and other times seeing ["DOC2"] is for a related reason. Your two separate arrays are both assigned to the same property doc.cards, which causes a conflict that you can see if you call Automerge.getConflicts(doc2, 'cards'). One of the two arrays gets picked to be the default resolution of the conflict that appears under the property doc.cards. The winner gets picked based on the actorIds of the two documents, and those are assigned randomly when you do Automerge.init(), so that's why you sometimes see one array and other times the other array.