Automerge: JSON-like data structure for building collaborative apps
github.com
github.com
The only thing I miss here is a clear spec of the CRDT itself so that people could implement compatible versions in other languages.
I haven't compared them though.
This is my second time highly recommending a Software Engineering Daily podcast with Martin Kleppman on it. He is really smart and able to break down really complex topics in a way that's easy to understand. If you've never heard of CRDTs before, have a listen - https://softwareengineeringdaily.com/2017/12/08/decentralize...
[0]: https://en.wikipedia.org/wiki/Conflict-free_replicated_data_...
Not really, obviously, but given that one of the big sub-topics of CSCW and CSCL is about designing for remote collaborative work, CRDT looks like a technology that naturally fits in there.
[0] https://en.wikipedia.org/wiki/Computer-supported_collaborati...
[1] https://en.wikipedia.org/wiki/Computer-supported_cooperative...
They're great, and not too hard to understand if you take time to think about them.
Here are some animated/interactive cartoon explainers we've made to help teach the concepts:
- How a CRDT version of Operational Transformation (Google Docs) works: http://gun.js.org/explainers/school/class.html
- Why CRDTs are better than centralized alternatives like PAXOS/RAFT: http://gun.js.org/distributed/matters.html
Martin Kleppmann's work (author of Automerge) is outstanding, especially check out: https://youtu.be/yCcWpzY8dIA?t=29m36s
I'm looking at Kleppermann's JSON work. But it doesn't look like it handles collaborative editing of strings very well: they have to either be treated as an immutable value in a register, which doesn't allow for collaborative editing of the string, or treated as a list of characters, which would have to represent large edits (e.g. deleting a word) as a sequence of character edits, which sounds inefficient. Kleppermann's work also handles maps, which I don't need, which is fine.
For context, I plan to make a tree/structure editor, and am considering representing the document as a CRDT.
Treating the long string like a list of substrings should work, I guess the optimum substring length could be determined experimentally based on real-world interaction patterns.
A pretty neat workable representation. Trivial to implement.
I'm surprised those apps haven't made a bigger effort to implement this logic for production. It would certainly make them unique and hard to leave.
CRDTs require the operations on them to commute or equivalently that state changes are monotonic. If your application fits those constraints you can probably build a quite elegant solution but unfortunately that is the exception rather than the norm, most operations are not commutative.
EDIT: This was meant to be a reply to [1] instead of a top-level comment.
What happens is that humans have intuitive coordination systems and tend to fix the same problems.
In other words, if the card title is "rename auotmerge" then both of us are likely to change it the same way, or at least either resolution is fine. In the event of a conflict we flag the multi-value in the UI so that a human user can decide if they care.
Where this approach shines is in domains where it helps avoid conflicts rather than resolving them. Naively adding replies to a comment thread might result in collisions or misordered comments across nodes of a distributed team. Automerge captures enough metadata from the operations created by users that changes "tend" to merge well.
I realize this is an argument not from first principles but from experience. We weren't able to find examples of other people building web applications with quite this approach (though we would love to hear from them if they are out there) and so the question was "will this feel alright?"
Apparently it does.
If you find this result provocative, I would encourage you to challenge it by attempting to build something with automerge and letting us know how it goes. I believe it will work well in applications where all user operations can always commute to produce a subjectively reasonable result. To make that concrete, a bank account is a poor domain to model with a tool like this, but a shared drawing is much better.
This might work to some extend for a toy example like a simple task management application but I would bet even then users would quickly get annoyed about losing their edits. And you also can not really claim that your are not losing edits because you keep them in a list of conflicts because for all practical purposes they are gone unless you build a solution to explicitly deal with them which renders trying to use CRDTs mood.
I think unless your operations map cleanly to CRDTs and you get the expected and correct behavior all the time, trying to use CRDTs will just cause additional pains and not provide any benefits. Trying to force general data structures with general, non-commuting operations and CRDTs together just seems ill-fated to me.
This means you can sync peer-to-peer without an Internet connection if say you have a Bluetooth connection between your phone and your laptop on an airplane.
Martin Kleppmann explains CRDT in detail in an episode of Software Engineering Daily.
https://softwareengineeringdaily.com/2017/12/08/decentralize...
Couchdb does it the right way, it simply keeps all versions and lets the application logic decide which is the "one true state".
If there are conflicts, it returns a _conflicts property on the object so you can retrieve the other conflicting alternatives so the application logic can write another change if it wants to use a different alternative than the one that was automatically chosen.
The linked project README, and even the CRDT Wikipedia page, seem to claim that conflicts are avoided, whereas, at least in my mind, these are more like strategies to automatically privilege one version in the event of a conflict.
// Pretend doc1 and doc2 are already Automerge objects
let doc1 = { x: 1 }
let doc2 = { x: 2 }
// x will be either 1 or 2 (arbitrarily chosen), but
// res1 will always == res2 regardless of choice
let res1 = Automerge.merge(doc1, doc2)
let res2 = Automerge.merge(doc2, doc1)
res1 == res2 // true
However, there may be cases where you want to manually resolve conflicts, and if that is the case, the `_conflicts` property is there so that you can undo whatever merge occurred automatically and set the "winner" yourself.doc1 = { 'a': {'b': {'c': ... }}}
doc2 = {}
So one has been deleted entirely and the other has modified some attribute deep down inside of it.
If you always have to check and resolve conflicts yourself, then it's not really useful.
If you want to build an editor you probably have to ask yourself what your 'attributes' are, but probably the individual letters.
Last year I built something fairly similar to automerge but with a focus on offline clients. I use it to sync my app's data from different clients to an WebDAV server. As some Clients are sometimes offline when changes occur the, resolving conflicts can occur easily but resolving them in an expected way isn't that hard if you do the time trick.
https://github.com/avian2/jsonmerge
https://www.tablix.org/~avian/blog/articles/talks/tomaz_solc...
To change the state, you can use the regular JavaScript array mutation
methods such as push(). Internally, Automerge translates this mutable API
call into an update of the immutable state object. Note that we must pass in
doc1, and get back an updated object which we assign to the same variable
doc1. The original document object is not modified.
Hmm. I like having the _option_ to use mutations to describe changes to the document, but in many cases I would actually prefer to use pure functions. Can I just return a new document with the necessary changes? I don't see any such examples in the docs here.> Different from git: no merge conflicts to resolve!
This is impossible unless there are significant restrictions on what kind of operations are possible.
If I have a bag of 15 apples, and I take 10 of those apples at the same time as somebody else takes more than five apples then we have a merge conflict right there because we can't end up with a negative amount of apples in the bag.
There is nothing new about doing things like this, OT and CRDT have existed for ages. Check out ShareDB (https://github.com/share/sharedb) or Webstrates (https://webstrates.net/) (based on ShareDB). In Webstrates, we don't have merge conflicts that need to be resolved and there are never any practical synchronization issues. The server orders the operation and if you try to delete something that's already been deleted, then we just ignore your operation.
Also quite courageous to say something is impossible when you have the code that does it right in front of you. ;-)
I will go read the docs in more detail but what you've just describes sounds pretty awful. Perhaps it's more just you being glib about non-cooperative actors in an environment that expects cooperatuon, but an "available balance" abstractions are a pretty reasonable thing to ask for, if only to encode natural numbers.
But surely, if you allow malicious actors to modify your document, then that's your problem right there.
Any particular reason why you choose CRDT over OT?
> The only case Automerge cannot handle automatically, because there is no well-defined resolution, is when users concurrently update the same property in the same object (or, similarly, the same index in the same list). In this case, Automerge arbitrarily picks one of the concurrently written values as the "winner":
1. CRDT like linear change rules.
2. Vector clocks.
3. Forking the entire tree and allowing merge queries, creating a DAG of the state that can be interactively and speculatively remerged.
Just picking one winner arbitrary and silently discarding all peers sounds like the kind of thing that gets labeled as a bug when we examine concurrent data stores.
Automerge must select one result to be the default consistently across all uncoordinated peers. If you don't do this, different nodes may see different documents during a conflict state, which is undesirable.
Wikipedia has some examples: https://en.m.wikipedia.org/wiki/Conflict-free_replicated_dat...
With OT, you send a na[1] (number add) operation, so this would work fine. Indeed, if you treat the number as a string, then you have a problem.
I bet you can run really far with this general idea. But there is no panacea.
To that end, the basics of math in associative, cumulative, distributive, etc., go together to create an algebra of what you can automate rather easily. I think most of us stopped thinking in terms of those laws years ago. To the point that it is probably odd to folks that stayed close to them.
I think you mean commutative. It certainly is associative.
Thanks!
Automerge has fairly robust support for these kinds of use-cases around lists, which we use quite a lot, but we haven't actually needed them for numbers (though I expect we may want them eventually.)
I wrote a middleware for Redux which propagates actions peer-to-peer in a consistent order using these timestamps and the scuttlebutt gossip protocol, you might find it interesting. https://github.com/grrowl/redux-scuttlebutt
If User A incremented 100, and saved it down; then User B loaded this saved state and incremented 101, it'd be 102.
For example, say you have a list AND a counter. Everytime you add an item to the list, you need to increment the counter, as an invariant of your system.
Of course, it's a contrived example, but you get the point: things can get more complicated and the system may break as a result, unless you're very careful.
But we merge by using differential synchronization (kinda) to resolve local changes and remote changes (to construct a "patch") by keeping an unchanged copy of the upstreams latest version (from the clients perspective).
> Different from git: no merge conflicts to resolve!
But later:
> The only case Automerge cannot handle automatically, because there is no well-defined resolution, is when users concurrently update the same property in the same object (or, similarly, the same index in the same list). In this case, Automerge arbitrarily picks one of the concurrently written values as the "winner":
...
> Although only one of the concurrently written values shows up in the object, the other values are not lost. They are merely relegated to a _conflicts object
>git merge --strategy-option=theirs
The domain I'm specifically thinking of is managing routing rules, like nginx configs or control-plane configs for Envoy. It's really tempting to simplify it down to a config file, then you version-control it, then you get merge conflicts, and all of a sudden a bad merge makes 10% of your services unroutable. Which is hilariously bad.
Having an auto-merge for those sort of configs seems like the only reasonable way to do it. Escalating conflicts up, from easy merge, to trying an automatic strategy, to surfacing conflicts to the user in a domain-specific way, seems like the only way to do it!
I.e. basically something like this but for text?
* https://github.com/wehriam/observed-remove
* https://github.com/wehriam/ipfs-observed-remove
The inherent tradeoffs can be challenging to explain. For example in these implementations, there are caching constraints on certain delete operations.
Automerge looks fantastic, and I look forward to more progress in the space.
More specifically, by Google Drive, I assume you mean Google Docs. And Google Docs doesn't use diffsync, it uses OT.
why doesn't google docs use DS? Who uses DS?
What's the best choice of making something similar to google docs' co-edit feature?
The wiki article includes information that should answer your last question as well.
https://operational-transformation.github.io/visualization.h...
it has a live visualization of the OT algorithm
Could git be configured with a different diff algorithm to have this functionality, or is it too tied to lines ?
That time I just used to write the whole JSON to firebase (with flock like locking) and it was really bad but since it was just a weekend project I didn't care much at that time. Bet a library like this could have saved me a lot of headache.
E.g. perhaps the most popular collaborative app is Google Docs. This would be a terrible choice for Google Docs, as it doesn't have specialized transforms for text editing/formatting (which turn out to be rather challenging).
But, this library looks nice, and I'm sure it can fit some use cases.
Another thing I don't like about CRDT is for text editing, it requires a uuid for each char you type. this is a huge waste of memory and disk imho. on the other hand OT requires no metadata
* How to integrate this with a decent pagination library?
* How to integrate this with vue.js?
* How does this compare to http://y-js.org ?
* How does this compare to http://gun.js.org ?
Thanks for your attention!