Crates.io | mergable |
lib.rs | mergable |
version | 0.45.0 |
source | src |
created_at | 2021-03-28 00:06:29.593366 |
updated_at | 2024-06-23 03:08:31.845006 |
description | A library for user-friendly and efficient CRDTs. |
homepage | https://gitlab.com/kevincox/mergable |
repository | https://gitlab.com/kevincox/mergable |
max_upload_size | |
id | 374452 |
size | 55,270 |
Easy, performant, pragmatic CRDTs.
WIP: Do not use.
In rough order of priority.
The key aspect of this is the Mergable
trait which has the following interface. The rest of the crate is data-types that implement the interface, and in the future tools to help you manage the structures and syncing.
Basic syncing is very simple. To send your changes to someone else just send them your structure. They can then call local.merge(remote)
and now they have your changes. They can send you their changes and you merge them in the same way and now you have the same structure. You can even do this in parallel!
let mut alice_state = unimplemented!();
let mut bob_state = unimplemented!();
// Send states to the other party.
let for_bob = serialize(&alice_state);
let for_alice = serialize(&bob_state);
// Update local states with remote.
alice_state.merge(deserialize(for_alice));
bob_state.merge(deserialize(for_bob));
assert_eq!(alice_state, bob_state);
For 1:1 syncing you maintain two copies of the data structure. One represents the remote state and one represents the local state. Edits are performed on the local state as desired. Occasionally a delta is generated and added to a sync queue.
let mut remote_state = unimplemented!();
let mut local_state = remote_state.clone(); // Or whatever you had lying around.
let mut sync_queue = Vec::new();
for _ in 0..10 {
make_changes(&mut local_state);
let delta = local_state.diff(&remote_state);
sync_queue.push(serialize(&delta));
remote_state.apply(delta.clone());
}
Once you have established a network connection the remote can apply your changes.
let mut remote_state = unimplemented!();
for delta in fetch_sync_queue() {
remote_state.apply(delta);
}
At this point both nodes have an identical structure. (Assuming that there have been no changes to the remote concurrently.)
This is a common pattern of having a central server as the "source of truth". This can be used to allow offline editing (like Google Docs) or with a P2P backup for max reliability. This pattern can provide the maximum efficiency for clients at the cost of some latency (for the edits to traverse the server).
For clients this looks exactly like 1:1
sync, a simple server looks roughly as follows.
struct Server<T: Mergable> {
state: T,
deltas: T::Diff,
}
impl<T: Mergable> Server<T> {
/// Get the current stae and version.
fn get(&self) -> (Mergable, usize) {
(self.state.clone(), self.deltas.len())
}
fn update(&mut self, diff: T::Diff) {
}
}
let mut state = unimplemented!();
let mut deltas = Vec::new();
let mut clients = Vec::new();
for event in unimplemented!("recieve events from clients") {
match event {
NewClient{client} => {
client.send(New{
state: serialize(&state),
version: delta.len(),
});
clients.push(client);
}
ResumeClient{client, version} => {
for (version, delta) in deltas.iter().enumerate().skip(version) {
client.send(Delta{
delta: &delta,
version: data.len(),
})
}
clients.push(client);
}
Delta{client_id, delta} => {
state.apply(deserialize(&delta));
deltas.push(delta);
for client in &mut clients {
client.send(Delta{
delta: &delta,
version: data.len(),
})
}
}
}
}
This is a simple implementation, some things could be approved.
New
state to old clients.)In the future I would like to provide a server core in this library to make a quality implementation easy.
This is currently tricky. The best option now is likely have all clients act as a server but care must be taken to avoid holding too much state in the clients.
Another option would be to keep a copy of the last-seen-state of all peers (for a period of time) and upon reconnection generate a delta based on that. This would be feasible if your data is not too large.
In the future we may add support for common-ancestor discovery which would allow for more efficient initial syncing. (This would be similar to how Git does pushes and pulls.)