loro 1.12.0

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

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

fn assert_path_json(doc: &LoroDoc, path: &[Index], expected: Value) {
    let value = doc.get_by_path(path).expect("path should resolve");
    assert_eq!(value.get_deep_value().to_json_value(), expected);
}

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

    let workspace = doc.get_map("workspace");
    let outline = workspace.insert_container("outline", LoroTree::new())?;
    let agenda = workspace.insert_container("agenda", LoroMovableList::new())?;

    outline.enable_fractional_index(0);
    let root = outline.create(TreeParentId::Root)?;
    let child_a = outline.create_at(root, 0)?;
    let child_b = outline.create_at(root, 1)?;
    outline.get_meta(root)?.insert("title", "root")?;
    outline.get_meta(child_a)?.insert("title", "child-a")?;
    outline.get_meta(child_b)?.insert("title", "child-b")?;

    let history = outline
        .get_meta(root)?
        .insert_container("history", LoroMovableList::new())?;
    history.push("born")?;
    history.push("grow")?;
    history.mov(0, 1)?;

    agenda.push("draft")?;
    agenda.push("review")?;
    agenda.push("ship")?;
    doc.commit();

    let expected_v1 = deep_json(&doc);
    let snapshot_v1 = doc.export(ExportMode::Snapshot)?;

    assert_eq!(outline.children(TreeParentId::Root).unwrap(), vec![root]);
    assert_eq!(outline.children(root).unwrap(), vec![child_a, child_b]);
    assert_eq!(
        history.get_deep_value().to_json_value(),
        json!(["grow", "born"])
    );
    assert_path_json(
        &doc,
        &[
            Index::Key("workspace".into()),
            Index::Key("outline".into()),
            Index::Seq(0),
            Index::Key("history".into()),
            Index::Seq(0),
        ],
        json!("grow"),
    );
    assert_eq!(
        doc.get_by_str_path("workspace/outline/0/history/0")
            .expect("string path should resolve")
            .get_deep_value()
            .to_json_value(),
        json!("grow")
    );
    let history_path = doc
        .get_path_to_container(&history.id())
        .expect("nested history list should have a path");
    assert!(history_path
        .iter()
        .any(|(_, index)| matches!(index, Index::Key(key) if key.as_str() == "workspace")));
    assert!(history_path
        .iter()
        .any(|(_, index)| matches!(index, Index::Key(key) if key.as_str() == "history")));

    let restored = LoroDoc::from_snapshot(&snapshot_v1)?;
    assert_eq!(deep_json(&restored), expected_v1);
    assert_path_json(
        &restored,
        &[
            Index::Key("workspace".into()),
            Index::Key("outline".into()),
            Index::Seq(0),
            Index::Key("history".into()),
            Index::Seq(0),
        ],
        json!("grow"),
    );

    outline.mov_before(child_b, child_a)?;
    outline.delete(child_a)?;
    agenda.mov(2, 0)?;
    agenda.set(1, "in-review")?;
    doc.commit();

    let expected_v2 = deep_json(&doc);
    let snapshot_v2 = doc.export(ExportMode::Snapshot)?;

    let restored_v2 = LoroDoc::from_snapshot(&snapshot_v2)?;
    assert_eq!(deep_json(&restored_v2), expected_v2);
    assert_path_json(
        &restored_v2,
        &[
            Index::Key("workspace".into()),
            Index::Key("outline".into()),
            Index::Seq(0),
            Index::Key("history".into()),
            Index::Seq(0),
        ],
        json!("grow"),
    );

    assert_eq!(outline.children(TreeParentId::Root).unwrap(), vec![root]);
    assert_eq!(outline.children(root).unwrap(), vec![child_b]);
    assert!(outline.is_node_deleted(&child_a)?);
    assert_eq!(
        agenda.get_deep_value().to_json_value(),
        json!(["ship", "in-review", "review"])
    );

    Ok(())
}

#[test]
fn movable_list_converges_after_sync_and_roundtrips_through_paths() -> LoroResult<()> {
    let base = LoroDoc::new();
    base.set_peer_id(21)?;
    let board = base.get_map("board");
    let tasks = board.insert_container("tasks", LoroMovableList::new())?;
    tasks.insert(0, "draft")?;
    tasks.insert(1, "review")?;
    tasks.insert(2, "ship")?;
    let note = tasks.push_container(LoroText::new())?;
    note.insert(0, "notes")?;
    base.commit();

    let base_updates = base.export(ExportMode::all_updates())?;
    let base_vv = base.oplog_vv();

    let a = LoroDoc::new();
    a.set_peer_id(22)?;
    a.import(&base_updates)?;
    let b = LoroDoc::new();
    b.set_peer_id(23)?;
    b.import(&base_updates)?;

    let a_tasks = LoroMovableList::try_from_container(
        a.get_map("board")
            .get("tasks")
            .expect("tasks should exist")
            .into_container()
            .expect("tasks should be a container"),
    )
    .expect("tasks should be a movable list");
    a_tasks.mov(2, 0)?;
    a.get_map("board").insert("status", "ready")?;
    a.commit();
    let a_update = a.export(ExportMode::updates(&base_vv))?;

    let b_tasks = LoroMovableList::try_from_container(
        b.get_map("board")
            .get("tasks")
            .expect("tasks should exist")
            .into_container()
            .expect("tasks should be a container"),
    )
    .expect("tasks should be a movable list");
    b_tasks.push("deploy")?;
    b.get_map("board").insert("priority", "p1")?;
    b.commit();
    let b_update = b.export(ExportMode::updates(&base_vv))?;

    a.import(&b_update)?;
    b.import(&a_update)?;
    assert_eq!(deep_json(&a), deep_json(&b));
    assert_eq!(
        deep_json(&a),
        json!({
            "board": {
                "priority": "p1",
                "status": "ready",
                "tasks": ["ship", "draft", "review", "notes", "deploy"]
            }
        })
    );

    let batch = LoroDoc::new();
    batch.set_peer_id(24)?;
    let status = batch.import_batch(&[a_update, b_update, base_updates])?;
    assert!(status.pending.is_none());
    assert_eq!(deep_json(&batch), deep_json(&a));
    assert_eq!(
        batch
            .get_by_str_path("board/tasks/0")
            .unwrap()
            .get_deep_value()
            .to_json_value(),
        json!("ship")
    );
    assert_eq!(
        batch
            .get_by_str_path("board/tasks/3")
            .unwrap()
            .get_deep_value()
            .to_json_value(),
        json!("notes")
    );
    assert_eq!(
        batch
            .get_by_str_path("board/tasks/4")
            .unwrap()
            .get_deep_value()
            .to_json_value(),
        json!("deploy")
    );

    Ok(())
}