Open HerbCaudill opened 3 years ago
I like the idea of this becoming a part of the library as it seems like a very easy thing to trip up on otherwise, I wonder if there are use cases where you wouldn't always want a common shared ancestor for all peers? I'd expect that in that case you could use separate documents.
Anyway I'd be for adding functionality to the init
function at least to create a default change in the backend which basically does nothing (perhaps initialises the root to be an empty map) with a common actor id. This should make syncing less confusing with overwriting histories.
Do we know how Yjs handles this? Just may be a point of reference.
I agree that we need a solution for this problem, but I'm not totally convinced that this approach is the best one. Special-casing the initialisation change to make it deterministic helps in the simple case when all peers perform exactly the same initialisation. However, if the developer ever changes the initialisation code, it will produce a different change with a different hash, and then we're back to square one again with conflicts on the top-level todos
key. If you are very careful you can have multiple initialisation changes that chain off each other, but it would be a very fragile API that is easy to use incorrectly.
I think a better approach would be to revive what was discussed years ago in #4: if two users concurrently create objects of the same type under the same key, then we don't make a conflict, but rather we merge the operations on those objects into a single object. That would have the desired effect in many common cases. There might still be some cases of initialisation where a deterministic initialisation change is needed, but they would be much less common.
if two users concurrently create objects of the same type under the same key, then we don't make a conflict, but rather we merge the operations on those objects into a single object
Thanks @ept - I knew something along these lines had come up in the past. I've reread #4 and I agree that's the right approach.
I think , we could create 2 new data type.
SharedArray and SharedMap (or other name like NoConflictArray , NoConflictMap
user A set data.todo = SharedArray(1) user B set data.todo = SharedArray(2) data.todo will be [1,2]
user A set data.todo = SharedArray(1) user B set data.todo = [2] data.todo will be [2] or SharedArray[1]
then we can add some action like makeConfictFreeList and makeConfictFreeMap default use makeList
@DiamondYuan That would be a big API change, which I'm not keen on. I think we can change the semantics for concurrently created objects on the same key without otherwise changing the API.
Suppose Alice initializes a document as follows:
Alice adds some todos:
Now Bob initializes his document using the same code as Alice:
Alice and Bob connect using the new sync protocol, perhaps using an implementation like this one. https://github.com/HerbCaudill/todomvc-automerge/blob/master/src/AutomergeSync.tsx
Half of the time, they will converge on Alice's list with two items; the other half, they will converge on Bob's empty list. It seems to me that the preferred outcome here is always going to be converge on Alice's list.
I asked about this on Slack and @ept suggested initializing the document with a "null" authorId like
0000
, and a timestamp of0
.conversation from slack fwiw
> @herbcaudill : > Continuing my exploration of Automerge 1, I've made a very minimal TodoMVC app in React, which can sync instances in multiple browsers using @localfirst/relay for networking. Code is here https://github.com/HerbCaudill/todomvc-automerge > > In my testing, if you make a bunch of changes to the list in one browser and then open another browser window, there's some % chance that the first browser's state will be overwritten with the default state from the second browser. > > IIRC, the way I'm doing this wouldn't have worked in Automerge 0.x : each client starts out with this as their state - > > ```ts > const defaultState = A.fromThis is the code I've ended up with, which solves the problem:
It seems like this is a fairly common use case, if not the most likely scenario & as such, it seems like it deserves better than this ugly user-land solution. A couple of possibilities come to mind:
My preferred solution would be to treat
init
andfrom
as special cases, with no author, and timestamp 0. Seems to me that it's not really meaningful to talk about "conflicts" in the context of the initial creation of the document. Alice's initialization and Bob's initialization are, for practical purposes, the same event. So basicallyinit
would have{ authorId: '0000' }
as the default options, andfrom
would have{ time: 0}
as the change options by default.Alternatively, we could expose my
initWithNullAuthor
as its own top-level Automerge function (hopefully with a better name!)