#![cfg(all(feature = "db-redb", feature = "iroh"))]
use atomic_lib::Storelike;
const CANVAS: &str = "https://atomicdata.dev/ontology/canvas/Canvas";
const STROKE_DATA: &str = "https://atomicdata.dev/ontology/canvas/strokeData";
const ROUNDS: usize = 40;
async fn new_canvas(store: &atomic_lib::Db, name: &str) -> String {
let (_agent, drive) = store.setup("Alice").await.unwrap();
store
.create_resource(
CANVAS,
&drive,
name,
Some(vec![(
STROKE_DATA,
atomic_lib::Value::Json(serde_json::Value::Array(vec![])),
)]),
)
.await
.unwrap()
}
async fn stroke_count(store: &atomic_lib::Db, subject: &str) -> usize {
let resource = store.get_resource(&subject.into()).await.unwrap();
match resource.get(STROKE_DATA) {
Ok(atomic_lib::Value::Json(v)) => v.as_array().map(|a| a.len()).unwrap_or(0),
_ => 0,
}
}
async fn local_edit(store: &atomic_lib::Db, canvas: &str, round: usize) {
let mut resource = store.get_resource(&canvas.into()).await.unwrap();
resource.ensure_materialized().unwrap();
resource
.push_list_item(STROKE_DATA, serde_json::json!({ "local": round }))
.unwrap();
resource.save_locally(store).await.unwrap();
}
async fn peer_apply(store: &atomic_lib::Db, canvas: &str, round: usize) {
let mut peer = store.get_resource(&canvas.into()).await.unwrap();
peer.ensure_materialized().unwrap();
peer.push_list_item(STROKE_DATA, serde_json::json!({ "remote": round }))
.unwrap();
let update = peer.materialized_state().unwrap();
if let Some(resolved) = atomic_lib::sync::ws_apply::resolve_update(store, canvas, &update).await
{
atomic_lib::sync::ws_apply::persist_update(store, canvas, resolved)
.await
.unwrap();
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_local_edit_racing_a_peer_update_keeps_both() {
let store = atomic_lib::Db::init_temp("concurrent_commit_race")
.await
.unwrap();
let canvas = new_canvas(&store, "Race canvas").await;
let local = {
let store = store.clone();
let canvas = canvas.clone();
tokio::spawn(async move {
for round in 0..ROUNDS {
local_edit(&store, &canvas, round).await;
tokio::task::yield_now().await;
}
})
};
let remote = {
let store = store.clone();
let canvas = canvas.clone();
tokio::spawn(async move {
for round in 0..ROUNDS {
peer_apply(&store, &canvas, round).await;
tokio::task::yield_now().await;
}
})
};
local.await.unwrap();
remote.await.unwrap();
let survived = stroke_count(&store, &canvas).await;
eprintln!("{survived} of {} operations survived", ROUNDS * 2);
assert_eq!(
survived,
ROUNDS * 2,
"a concurrent local commit and peer apply clobbered each other"
);
}
#[tokio::test]
async fn the_same_operations_sequentially_keep_everything() {
let store = atomic_lib::Db::init_temp("concurrent_commit_control")
.await
.unwrap();
let canvas = new_canvas(&store, "Control canvas").await;
for round in 0..ROUNDS {
local_edit(&store, &canvas, round).await;
peer_apply(&store, &canvas, round).await;
}
assert_eq!(
stroke_count(&store, &canvas).await,
ROUNDS * 2,
"sequential local edits and peer applies must not lose anything"
);
}