There's actually an approach that sits in between this and formal CRDTs.
I had the same insight that having a total ordered log of changes solves a lot of the issues with concurrent updates. We solved this by creating one sequence CRDT that ensures all events from clients eventually end up in the same order and then our data structures are just reductions over those events. It's a little bit like a distributed, synchronized Redux store. This let us avoid having to think in terms of CRDT types (LWW registers, Grow-only sets, sequences, etc) and just think in terms of the domain-specific business logic (e.g. "what should happen when someone marks a todo as done, after it's deleted?").
There's a little but more to so if this is interesting to anyone, I can write a more detailed explanation.
Have you considered backing your application's state-synchronization with a centralized CQRS/ES event store (e.g. https://www.eventstore.com/)? You get the same semantics, without paying the overhead of building them on top of a CRDT abstraction; and the event store itself also "knows" (in the sense that it supplies the framework and you supply the logic) how to reduce over states in order to build snapshot states ("aggregates") for clients to download for fast initial synchronization.
We basically have that as well! We use CRDTs on the client to allow for optimistic updates and fast replication of events directly to other clients but we also do exactly what you describe on the server so that a user loading the data for the first time just gets the "snapshot" and then plays events on top of it.
So each "event" has three properties that allow us to resolve to a sensible order across clients: session (integer), device_id (uuid), order (integer). The `session` number is set by the highest `order` that your device has seen from the server and the `order` gets incremented on each new event.
So an example you might have a sequence of events like this.
[session] [order]
0 1
0 2
0 3
---sync ---
3 4
3 5
3 6
We can then sort all events by (session, device_id, order) and we'll get any events that happen on a device to be sorted in a row even if some other device created a bunch of concurrent events at the same time.
What about situations where two different clients both begin to publish new events from the same starting point? Those events can't be absolutely ordered right? If you're thinking of Lamport-style happens-before relations, then you can't enforce total ordering of those events. Do you just arbitrarily mark one of those client event streams as failed and force the client to absorb new state and try again?
They do, you're looking for reading around distributed systems topics. Lamport timestamps, a type of logical timestamp, are explained on Wikipedia [1]. You can use these to implement Vector Clocks [2]. I personally learned this by reading papers in undergrad and grad school as I researched in distributed systems, but I hear good things about Steen and Tanenbaum's Distributed Systems [3].
I had the same insight that having a total ordered log of changes solves a lot of the issues with concurrent updates. We solved this by creating one sequence CRDT that ensures all events from clients eventually end up in the same order and then our data structures are just reductions over those events. It's a little bit like a distributed, synchronized Redux store. This let us avoid having to think in terms of CRDT types (LWW registers, Grow-only sets, sequences, etc) and just think in terms of the domain-specific business logic (e.g. "what should happen when someone marks a todo as done, after it's deleted?").
There's a little but more to so if this is interesting to anyone, I can write a more detailed explanation.