loro 1.12.0

Loro is a high-performance CRDTs framework. Make your app collaborative efforlessly.
Documentation
use loro::{ExportMode, Index, LoroDoc, LoroMap, LoroMovableList, LoroResult, LoroText, ToJson};
use pretty_assertions::assert_eq;
use serde_json::{json, Value};

fn deep_json(doc: &LoroDoc) -> Value {
    doc.get_deep_value().to_json_value()
}

#[test]
fn movable_list_diff_apply_preserves_moves_container_replacements_and_reverse_patch(
) -> LoroResult<()> {
    let doc = LoroDoc::new();
    doc.set_peer_id(91)?;

    let list = doc.get_movable_list("queue");
    list.push("alpha")?;
    let first_map = list.push_container(LoroMap::new())?;
    first_map.insert("id", "first")?;
    first_map.insert("rank", 1)?;
    let second_text = list.push_container(LoroText::new())?;
    second_text.insert(0, "second")?;
    list.push("tail")?;
    doc.commit();

    let v1 = doc.state_frontiers();
    let snapshot_v1 = doc.export(ExportMode::Snapshot)?;
    let value_v1 = deep_json(&doc);
    assert_eq!(
        value_v1,
        json!({
            "queue": ["alpha", {"id": "first", "rank": 1}, "second", "tail"]
        })
    );

    list.mov(2, 0)?;
    let replacement = list.insert_container(1, LoroMap::new())?;
    replacement.insert("id", "replacement")?;
    replacement.insert("rank", 2)?;
    list.delete(3, 1)?;
    list.set(3, "tail-updated")?;
    doc.commit();

    let v2 = doc.state_frontiers();
    let snapshot_v2 = doc.export(ExportMode::Snapshot)?;
    let value_v2 = deep_json(&doc);
    assert_eq!(
        value_v2,
        json!({
            "queue": ["second", {"id": "replacement", "rank": 2}, "alpha", "tail-updated"]
        })
    );

    let forward = doc.diff(&v1, &v2)?;
    let patched = LoroDoc::from_snapshot(&snapshot_v1)?;
    patched.apply_diff(forward)?;
    assert_eq!(deep_json(&patched), value_v2);

    let reverse = doc.diff(&v2, &v1)?;
    let restored = LoroDoc::from_snapshot(&snapshot_v2)?;
    restored.apply_diff(reverse)?;
    assert_eq!(deep_json(&restored), value_v1);

    Ok(())
}

#[test]
fn movable_list_creator_mover_and_editor_metadata_survives_remote_imports() -> LoroResult<()> {
    let doc = LoroDoc::new();
    doc.set_peer_id(201)?;
    let list = doc.get_movable_list("queue");
    list.push("draft")?;
    list.push("review")?;
    list.push("ship")?;
    let note = list.push_container(LoroText::new())?;
    note.insert(0, "note")?;
    doc.commit();

    let base_vv = doc.oplog_vv();
    for index in 0..list.len() {
        assert_eq!(list.get_creator_at(index), Some(201));
        assert_eq!(list.get_last_mover_at(index), Some(201));
        assert_eq!(list.get_last_editor_at(index), Some(201));
    }

    let remote = doc.fork();
    remote.set_peer_id(202)?;
    let remote_list = remote.get_movable_list("queue");
    remote_list.mov(2, 0)?;
    remote_list.set(1, "draft-updated")?;
    let replacement = remote_list.set_container(3, LoroMap::new())?;
    replacement.insert("kind", "replacement")?;
    remote.commit();

    let updates = remote.export(ExportMode::updates(&base_vv))?;
    doc.import(&updates)?;

    assert_eq!(
        list.get_deep_value().to_json_value(),
        json!(["ship", "draft-updated", "review", {"kind": "replacement"}])
    );
    assert_eq!(list.get_creator_at(0), Some(201));
    assert_eq!(list.get_last_mover_at(0), Some(202));
    assert_eq!(list.get_last_editor_at(0), Some(201));

    assert_eq!(list.get_creator_at(1), Some(201));
    assert_eq!(list.get_last_editor_at(1), Some(202));
    assert_eq!(list.get_creator_at(3), Some(201));
    assert_eq!(list.get_last_editor_at(3), Some(202));

    let restored = LoroDoc::from_snapshot(&doc.export(ExportMode::Snapshot)?)?;
    let restored_list = restored.get_movable_list("queue");
    assert_eq!(restored_list.get_creator_at(0), Some(201));
    assert_eq!(restored_list.get_last_mover_at(0), Some(202));
    assert_eq!(restored_list.get_last_editor_at(1), Some(202));
    assert_eq!(restored_list.get_last_editor_at(3), Some(202));
    assert_eq!(deep_json(&restored), deep_json(&doc));

    Ok(())
}

#[test]
fn movable_list_diff_apply_roundtrips_mixed_values_and_path_lookups() -> LoroResult<()> {
    let doc = LoroDoc::new();
    doc.set_peer_id(202)?;

    let root = doc.get_map("root");
    let queue = root.insert_container("queue", LoroMovableList::new())?;

    let first = queue.push_container(LoroMap::new())?;
    first.insert("name", "alpha")?;
    first.insert("bytes", vec![1_u8, 2, 3, 255])?;

    let second = queue.push_container(LoroText::new())?;
    second.insert(0, "beta")?;

    queue.push(true)?;
    doc.commit();

    let before = doc.state_frontiers();
    let before_snapshot = doc.export(ExportMode::Snapshot)?;
    let before_doc = LoroDoc::from_snapshot(&before_snapshot)?;

    assert_eq!(
        doc.get_by_str_path("root/queue/0/name")
            .expect("map child should resolve by path")
            .get_deep_value()
            .to_json_value(),
        json!("alpha")
    );
    assert_eq!(
        doc.get_by_path(&[
            Index::Key("root".into()),
            Index::Key("queue".into()),
            Index::Seq(1),
        ])
        .expect("text child should resolve by path")
        .get_deep_value()
        .to_json_value(),
        json!("beta")
    );
    assert_eq!(
        doc.get_by_str_path("root/queue/2")
            .expect("scalar value should resolve by path")
            .get_deep_value()
            .to_json_value(),
        json!(true)
    );

    queue.mov(1, 0)?;
    first.insert("rank", 1)?;
    queue.set(2, false)?;
    let third = queue.set_container(2, LoroMap::new())?;
    third.insert("kind", "replacement")?;
    doc.commit();

    let after = doc.state_frontiers();
    let after_snapshot = doc.export(ExportMode::Snapshot)?;

    assert_eq!(
        doc.get_by_str_path("root/queue/1/name")
            .expect("moved map child should still resolve")
            .get_deep_value()
            .to_json_value(),
        json!("alpha")
    );
    assert_eq!(
        doc.get_by_str_path("root/queue/0")
            .expect("moved text child should resolve")
            .get_deep_value()
            .to_json_value(),
        json!("beta")
    );
    assert_eq!(
        doc.get_by_str_path("root/queue/2/kind")
            .expect("replacement map should resolve")
            .get_deep_value()
            .to_json_value(),
        json!("replacement")
    );
    assert_eq!(
        doc.get_deep_value().to_json_value(),
        json!({
            "root": {
                "queue": [
                    "beta",
                    {"name": "alpha", "bytes": [1, 2, 3, 255], "rank": 1},
                    {"kind": "replacement"}
                ]
            }
        })
    );

    let forward = doc.diff(&before, &after)?;
    let patched = LoroDoc::from_snapshot(&before_snapshot)?;
    patched.apply_diff(forward)?;
    assert_eq!(deep_json(&patched), deep_json(&doc));

    let reverse = doc.diff(&after, &before)?;
    let restored = LoroDoc::from_snapshot(&after_snapshot)?;
    restored.apply_diff(reverse)?;
    assert_eq!(deep_json(&restored), deep_json(&before_doc));

    assert_eq!(queue.get_creator_at(0), Some(202));
    assert_eq!(queue.get_last_mover_at(0), Some(202));
    assert_eq!(queue.get_last_editor_at(1), Some(202));

    Ok(())
}

#[test]
fn movable_list_pop_and_out_of_bounds_set_container_follow_contract() -> LoroResult<()> {
    let doc = LoroDoc::new();
    let list = doc.get_movable_list("queue");
    assert!(list.pop()?.is_none());

    list.push("a")?;
    list.push("b")?;
    assert_eq!(
        list.pop()?.unwrap().get_deep_value().to_json_value(),
        json!("b")
    );
    assert_eq!(
        list.pop()?.unwrap().get_deep_value().to_json_value(),
        json!("a")
    );
    assert!(list.pop()?.is_none());
    assert!(list.set_container(0, LoroMap::new()).is_err());

    let detached = loro::LoroMovableList::new();
    assert!(detached.pop()?.is_none());
    assert!(detached.set_container(0, LoroMap::new()).is_err());
    detached.push("x")?;
    let map = detached.set_container(0, LoroMap::new())?;
    map.insert("ok", true)?;
    assert_eq!(
        detached.get_deep_value().to_json_value(),
        json!([{"ok": true}])
    );

    Ok(())
}