loro 1.12.0

Loro is a high-performance CRDTs framework. Make your app collaborative efforlessly.
Documentation
use std::ops::ControlFlow;

use loro::{
    ChangeTravelError, CommitOptions, ContainerTrait, ExportMode, Frontiers, Index, LoroDoc,
    LoroList, LoroMap, LoroText, ToJson, TreeParentId, ID,
};
use pretty_assertions::assert_eq;
use serde_json::json;

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

#[test]
fn doc_analysis_compaction_and_state_correctness_follow_contract() -> anyhow::Result<()> {
    let doc = LoroDoc::new();
    doc.set_peer_id(101)?;
    doc.set_record_timestamp(true);
    doc.set_change_merge_interval(0);

    let map = doc.get_map("root");
    map.insert("title", "draft")?;
    let text = map.insert_container("text", LoroText::new())?;
    text.insert(0, "hello")?;
    let list = map.insert_container("items", LoroList::new())?;
    list.push("one")?;
    list.push("two")?;
    doc.commit_with(CommitOptions::new().timestamp(10).commit_msg("seed"));

    let tree = doc.get_tree("tree");
    tree.enable_fractional_index(0);
    let root = tree.create(TreeParentId::Root)?;
    tree.get_meta(root)?.insert("kind", "root")?;
    doc.commit_with(CommitOptions::new().timestamp(20).commit_msg("tree"));

    let nested = list.push_container(LoroMap::new())?;
    nested.insert("name", "nested")?;
    text.insert(text.len_unicode(), " world")?;
    doc.commit_with(CommitOptions::new().timestamp(30).commit_msg("extend"));

    let before_compact = json_value(&doc);
    let analysis = doc.analyze();
    assert!(!analysis.is_empty());
    assert_eq!(analysis.dropped_len(), 0);
    assert!(analysis.tiny_container_len() > 0);
    assert!(
        analysis
            .containers
            .get(&map.id())
            .expect("root map should be analyzed")
            .ops_num
            > 0
    );
    assert!(
        analysis
            .containers
            .get(&text.id())
            .expect("text should be analyzed")
            .last_edit_time
            >= 30
    );

    doc.check_state_correctness_slow();
    doc.compact_change_store();
    assert_eq!(json_value(&doc), before_compact);
    doc.check_state_correctness_slow();

    let snapshot = doc.export(ExportMode::Snapshot)?;
    let imported = LoroDoc::from_snapshot(&snapshot)?;
    imported.check_state_correctness_slow();
    assert_eq!(json_value(&imported), before_compact);

    Ok(())
}

#[test]
fn checkout_history_cache_and_diff_application_follow_contract() -> anyhow::Result<()> {
    let doc = LoroDoc::new();
    doc.set_peer_id(102)?;
    doc.set_change_merge_interval(0);

    let text = doc.get_text("text");
    let list = doc.get_list("list");
    let map = doc.get_map("map");

    let v0 = doc.state_frontiers();
    text.insert(0, "abc")?;
    list.push("a")?;
    map.insert("phase", "one")?;
    doc.commit();
    let v1 = doc.state_frontiers();

    text.insert(1, "XYZ")?;
    list.insert(1, "b")?;
    map.insert("phase", "two")?;
    doc.commit();
    let v2 = doc.state_frontiers();
    let expected_v2 = json_value(&doc);

    let diff_v0_v2 = doc.diff(&v0, &v2)?;
    let replay = LoroDoc::new();
    replay.apply_diff(diff_v0_v2)?;
    assert_eq!(json_value(&replay), expected_v2);

    doc.checkout(&v1)?;
    assert!(doc.is_detached());
    assert_eq!(
        json_value(&doc),
        json!({"list": ["a"], "map": {"phase": "one"}, "text": "abc"})
    );
    assert!(doc.has_history_cache());

    doc.free_history_cache();
    assert!(!doc.has_history_cache());
    doc.free_diff_calculator();

    doc.checkout_to_latest();
    assert!(!doc.is_detached());
    assert_eq!(json_value(&doc), expected_v2);

    doc.revert_to(&v1)?;
    assert_eq!(
        json_value(&doc),
        json!({"list": ["a"], "map": {"phase": "one"}, "text": "abc"})
    );

    Ok(())
}

#[test]
fn change_traversal_exposes_changed_containers_and_id_spans() -> anyhow::Result<()> {
    let doc = LoroDoc::new();
    doc.set_peer_id(103)?;
    doc.set_change_merge_interval(0);

    let root = doc.get_map("root");
    root.insert("title", "first")?;
    doc.commit_with(CommitOptions::new().commit_msg("first"));
    let first = doc
        .state_frontiers()
        .as_single()
        .expect("single local commit frontier");
    let first_change = doc.get_change(first).expect("first change should exist");

    let text = root.insert_container("body", LoroText::new())?;
    text.insert(0, "hello")?;
    doc.commit_with(CommitOptions::new().commit_msg("second"));
    let second = doc
        .state_frontiers()
        .as_single()
        .expect("single local commit frontier");
    let second_change = doc.get_change(second).expect("second change should exist");

    let list = root.insert_container("items", LoroList::new())?;
    list.push("a")?;
    doc.commit_with(CommitOptions::new().commit_msg("third"));
    let third = doc
        .state_frontiers()
        .as_single()
        .expect("single local commit frontier");

    let changed_first = doc.get_changed_containers_in(first_change.id, first_change.len);
    assert!(changed_first.contains(&root.id()));

    let changed_second = doc.get_changed_containers_in(second_change.id, second_change.len);
    assert!(changed_second.contains(&text.id()));

    let between = doc.find_id_spans_between(&Frontiers::from_id(first), &Frontiers::from_id(third));
    assert_eq!(
        between.forward.get(&103).map(|span| (span.start, span.end)),
        Some((first.counter + 1, third.counter + 1))
    );

    let mut messages = Vec::new();
    doc.travel_change_ancestors(&[third], &mut |change| {
        messages.push(change.message().to_string());
        ControlFlow::Continue(())
    })?;
    assert_eq!(messages, vec!["third", "second", "first"]);

    let mut first_two = Vec::new();
    doc.travel_change_ancestors(&[third], &mut |change| {
        first_two.push(change.message().to_string());
        if first_two.len() == 2 {
            ControlFlow::Break(())
        } else {
            ControlFlow::Continue(())
        }
    })?;
    assert_eq!(first_two, vec!["third", "second"]);

    Ok(())
}

#[test]
fn version_comparison_travel_errors_and_snapshot_mode_contracts() -> anyhow::Result<()> {
    let doc = LoroDoc::new();
    doc.set_peer_id(106)?;
    doc.set_change_merge_interval(0);

    let root = doc.get_map("root");
    assert_eq!(doc.get_pending_txn_len(), 0);
    root.insert("phase", "one")?;
    assert_eq!(doc.get_pending_txn_len(), 1);
    let exported_from_pending_txn = doc.export(ExportMode::all_updates())?;
    assert_eq!(doc.get_pending_txn_len(), 0);
    assert!(LoroDoc::from_snapshot(&exported_from_pending_txn).is_err());
    let first = doc.state_frontiers();
    let first_id = first.as_single().expect("first commit frontier");

    root.insert("phase", "two")?;
    doc.commit_with(CommitOptions::new().commit_msg("second"));
    let second = doc.state_frontiers();
    let second_id = second.as_single().expect("second commit frontier");

    assert_eq!(
        doc.cmp_frontiers(&first, &second)
            .expect("frontiers should be included"),
        Some(std::cmp::Ordering::Less)
    );
    assert_eq!(doc.cmp_with_frontiers(&second), std::cmp::Ordering::Equal);
    assert!(doc
        .cmp_frontiers(&Frontiers::from_id(ID::new(999, 0)), &second)
        .is_err());

    let missing =
        doc.travel_change_ancestors(&[ID::new(106, 999)], &mut |_| ControlFlow::Continue(()));
    assert!(matches!(
        missing,
        Err(ChangeTravelError::TargetIdNotFound(_))
    ));

    let shallow_blob = doc.export(ExportMode::shallow_snapshot(&second))?;
    let shallow = LoroDoc::new();
    shallow.import(&shallow_blob)?;
    let shallow_err =
        shallow.travel_change_ancestors(&[first_id], &mut |_| ControlFlow::Continue(()));
    assert!(matches!(
        shallow_err,
        Err(ChangeTravelError::TargetVersionNotIncluded)
    ));

    let mut visited = Vec::new();
    doc.travel_change_ancestors(&[second_id], &mut |change| {
        visited.push(change.id);
        ControlFlow::Continue(())
    })?;
    assert_eq!(visited.first(), Some(&second_id));
    assert!(visited.contains(&first_id));

    Ok(())
}

#[test]
fn shallow_doc_state_check_and_export_boundaries_follow_contract() -> anyhow::Result<()> {
    let doc = LoroDoc::new();
    doc.set_peer_id(104)?;
    doc.set_change_merge_interval(0);

    let text = doc.get_text("text");
    text.insert(0, "alpha")?;
    doc.commit_with(CommitOptions::new().timestamp(1));
    let shallow_start = doc.state_frontiers();

    text.insert(text.len_unicode(), "-beta")?;
    doc.get_map("meta").insert("stage", "beta")?;
    doc.commit_with(CommitOptions::new().timestamp(2));
    let expected_latest = json_value(&doc);

    let shallow_blob = doc.export(ExportMode::shallow_snapshot(&shallow_start))?;
    let shallow = LoroDoc::new();
    shallow.import(&shallow_blob)?;
    assert!(shallow.is_shallow());
    assert_eq!(shallow.shallow_since_frontiers(), shallow_start);
    assert_eq!(json_value(&shallow), expected_latest);
    shallow.check_state_correctness_slow();

    let latest_updates = doc.export(ExportMode::updates(&shallow.shallow_since_vv().to_vv()))?;
    let imported = LoroDoc::new();
    imported.import(&shallow_blob)?;
    imported.import(&latest_updates)?;
    assert_eq!(json_value(&imported), expected_latest);

    Ok(())
}

#[test]
fn path_queries_and_root_values_preserve_container_contracts() -> anyhow::Result<()> {
    let doc = LoroDoc::new();
    let root = doc.get_map("root");
    let list = root.insert_container("items", LoroList::new())?;
    let child = list.push_container(LoroMap::new())?;
    child.insert("name", "leaf")?;
    let text = child.insert_container("body", LoroText::new())?;
    text.insert(0, "content")?;

    assert_eq!(
        doc.get_by_path(&[
            Index::Key("root".into()),
            Index::Key("items".into()),
            Index::Seq(0),
            Index::Key("name".into())
        ])
        .unwrap()
        .into_value()
        .unwrap(),
        "leaf".into()
    );
    assert_eq!(
        doc.get_by_str_path("root/items/0/body")
            .unwrap()
            .into_container()
            .unwrap()
            .id(),
        text.id()
    );
    assert!(doc.get_by_str_path("root/items/9").is_none());
    assert!(doc.get_by_str_path("root/items/0/body/missing").is_none());

    let shallow = doc.get_value().to_json_value();
    let deep = doc.get_deep_value().to_json_value();
    let deep_with_id = doc.get_deep_value_with_id().to_json_value();
    assert_ne!(shallow, deep);
    assert_eq!(
        deep,
        json!({"root": {"items": [{"body": "content", "name": "leaf"}]}})
    );
    assert!(deep_with_id["root"]["cid"].is_string());
    assert_eq!(
        deep_with_id["root"]["value"]["items"]["value"][0]["value"]["body"]["value"],
        json!("content")
    );

    Ok(())
}

#[test]
fn deleted_containers_snapshot_and_compaction_follow_contract() -> anyhow::Result<()> {
    let doc = LoroDoc::new();
    doc.set_peer_id(108)?;
    doc.set_change_merge_interval(0);

    let root = doc.get_map("root");
    let body = root.insert_container("body", LoroText::new())?;
    body.insert(0, "hello")?;
    let items = root.insert_container("items", LoroList::new())?;
    items.push("one")?;
    items.push("two")?;
    doc.commit();

    let body_id = body.id();
    let items_id = items.id();
    assert_eq!(
        doc.get_path_to_container(&body_id)
            .unwrap()
            .last()
            .unwrap()
            .1,
        Index::Key("body".into())
    );
    assert_eq!(
        doc.get_path_to_container(&items_id)
            .unwrap()
            .last()
            .unwrap()
            .1,
        Index::Key("items".into())
    );

    root.delete("body")?;
    root.delete("items")?;
    doc.commit();

    let expected = json_value(&doc);
    assert_eq!(expected, json!({"root": {}}));
    doc.check_state_correctness_slow();

    let snapshot = doc.export(ExportMode::Snapshot)?;
    let restored = LoroDoc::from_snapshot(&snapshot)?;
    assert_eq!(json_value(&restored), expected);
    restored.check_state_correctness_slow();

    restored.compact_change_store();
    assert_eq!(json_value(&restored), expected);
    restored.check_state_correctness_slow();

    let shallow = doc.export(ExportMode::state_only(Some(&doc.state_frontiers())))?;
    let shallow_doc = LoroDoc::new();
    shallow_doc.import(&shallow)?;
    assert!(shallow_doc.is_shallow());
    assert_eq!(json_value(&shallow_doc), expected);

    Ok(())
}