Fix undo with checkout (#375)

* fix: should transform checkout event

* chore: update fuzz dep

* chore: add pos to error info

* fix: clear undo/redo stack when checkingout

* test: update fuzz dep

* test: a new failed test case

* fix: tree transform

* chore: fuzz

* chore: add log

* chore: add more logs

* fix: compose err

* chore: fuzz test dep

* test: a failed tree case

* fix: undo tree event

* fix: do not compare tree position in fuzz

* fix: fuzz rev

* test: a failed tree case

* fix: add tree compose

* chore: add comment

* chore: fuzz

* fix: test

* fix: tree transform

* fix: tree transform index

* fix: sort tree index

* chore: fuzz

* fix: undo/redo remote change effect compose

* bk

* fix: tree undo redo (#385)

* fix: event hint none

* chore: fuzz version

* ci: fuzz

* bk: weird err

* fix: type err

* fix: fractional index between

* fix: wasm counter feature

* test: a new failed case

* fix: recursively create child nodes

* fix: filter empty event

* bk

* bk

* fix: tree undo redo remap

* chore: clean

* bk

* fix: tree need remap first

* fix: tree undo effect

* fix: tree diff calc

* fix: tree fuzz check eq func

* fix: remove EventHint None

* chore: cargo fix

* fix: tree uncreate

* fix: fuzz tree assert only structure

* refactor: rename methods

* fix: movable tree apply delta

* fix: another movable list issue

* chore: fuzz only check 1 actor's history

---------

Co-authored-by: Leon Zhao <leeeon233@gmail.com>
This commit is contained in:
Zixuan Chen 2024-07-04 18:15:44 +08:00 committed by GitHub
parent fc46f429f1
commit 9047065843
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 7453 additions and 491 deletions

29
Cargo.lock generated
View file

@ -661,8 +661,7 @@ dependencies = [
"fxhash",
"itertools 0.12.1",
"loro 0.16.2",
"loro 0.16.2 (git+https://github.com/loro-dev/loro.git?branch=main)",
"loro-common 0.16.2 (git+https://github.com/loro-dev/loro.git?branch=main)",
"loro 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a)",
"rand",
"serde_json",
"tabled 0.10.0",
@ -999,13 +998,13 @@ dependencies = [
[[package]]
name = "loro"
version = "0.16.2"
source = "git+https://github.com/loro-dev/loro.git?branch=main#afac34755f3f9a099c2985ff22dc9f2534d72290"
source = "git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a#39caa6cedf66d8f6c77f0886e70ec1270a96bd1a"
dependencies = [
"either",
"enum-as-inner 0.6.0",
"generic-btree",
"loro-delta 0.16.2 (git+https://github.com/loro-dev/loro.git?branch=main)",
"loro-internal 0.16.2 (git+https://github.com/loro-dev/loro.git?branch=main)",
"loro-delta 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a)",
"loro-internal 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a)",
"tracing",
]
@ -1029,12 +1028,12 @@ dependencies = [
[[package]]
name = "loro-common"
version = "0.16.2"
source = "git+https://github.com/loro-dev/loro.git?branch=main#afac34755f3f9a099c2985ff22dc9f2534d72290"
source = "git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a#39caa6cedf66d8f6c77f0886e70ec1270a96bd1a"
dependencies = [
"arbitrary",
"enum-as-inner 0.6.0",
"fxhash",
"loro-rle 0.16.2 (git+https://github.com/loro-dev/loro.git?branch=main)",
"loro-rle 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a)",
"nonmax",
"serde",
"serde_columnar",
@ -1061,7 +1060,7 @@ dependencies = [
[[package]]
name = "loro-delta"
version = "0.16.2"
source = "git+https://github.com/loro-dev/loro.git?branch=main#afac34755f3f9a099c2985ff22dc9f2534d72290"
source = "git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a#39caa6cedf66d8f6c77f0886e70ec1270a96bd1a"
dependencies = [
"arrayvec",
"enum-as-inner 0.5.1",
@ -1124,7 +1123,7 @@ dependencies = [
[[package]]
name = "loro-internal"
version = "0.16.2"
source = "git+https://github.com/loro-dev/loro.git?branch=main#afac34755f3f9a099c2985ff22dc9f2534d72290"
source = "git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a#39caa6cedf66d8f6c77f0886e70ec1270a96bd1a"
dependencies = [
"append-only-bytes",
"arref",
@ -1137,10 +1136,10 @@ dependencies = [
"im",
"itertools 0.12.1",
"leb128",
"loro-common 0.16.2 (git+https://github.com/loro-dev/loro.git?branch=main)",
"loro-delta 0.16.2 (git+https://github.com/loro-dev/loro.git?branch=main)",
"loro-rle 0.16.2 (git+https://github.com/loro-dev/loro.git?branch=main)",
"loro_fractional_index 0.16.2 (git+https://github.com/loro-dev/loro.git?branch=main)",
"loro-common 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a)",
"loro-delta 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a)",
"loro-rle 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a)",
"loro_fractional_index 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a)",
"md5",
"num",
"num-derive",
@ -1176,7 +1175,7 @@ dependencies = [
[[package]]
name = "loro-rle"
version = "0.16.2"
source = "git+https://github.com/loro-dev/loro.git?branch=main#afac34755f3f9a099c2985ff22dc9f2534d72290"
source = "git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a#39caa6cedf66d8f6c77f0886e70ec1270a96bd1a"
dependencies = [
"append-only-bytes",
"arref",
@ -1225,7 +1224,7 @@ dependencies = [
[[package]]
name = "loro_fractional_index"
version = "0.16.2"
source = "git+https://github.com/loro-dev/loro.git?branch=main#afac34755f3f9a099c2985ff22dc9f2534d72290"
source = "git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a#39caa6cedf66d8f6c77f0886e70ec1270a96bd1a"
dependencies = [
"imbl",
"rand",

View file

@ -74,7 +74,7 @@ pub(crate) fn new_after(bytes: &[u8]) -> Vec<u8> {
}
pub(crate) fn new_between(left: &[u8], right: &[u8], extra_capacity: usize) -> Option<Vec<u8>> {
let shorter_len = left.len().min(right.len());
let shorter_len = left.len().min(right.len()) - 1;
for i in 0..shorter_len {
if left[i] < right[i] - 1 {
let mut ans: Vec<u8> = left[0..=i].into();

View file

@ -7,18 +7,8 @@ publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
loro-without-counter = { path = "../loro", package = "loro" }
loro = { git = "https://github.com/loro-dev/loro.git", features = [
"counter",
], branch = "main" }
loro-common = { git = "https://github.com/loro-dev/loro.git", features = [
"counter",
], branch = "main" }
# loro = { path = "../loro", package = "loro", features = ["counter"] }
# loro-common = { path = "../loro-common", package = "loro-common", features = [
# "counter",
# ] }
# loro-without-counter = { git = "https://github.com/loro-dev/loro.git", branch = "main", package = "loro" }
loro = { path = "../loro", features = ["counter"], package = "loro" }
loro-without-counter = { git = "https://github.com/loro-dev/loro.git", rev = "39caa6cedf66d8f6c77f0886e70ec1270a96bd1a", package = "loro", default-features = false }
fxhash = { workspace = true }
enum_dispatch = { workspace = true }
enum-as-inner = { workspace = true }
@ -27,6 +17,7 @@ itertools = { workspace = true }
arbitrary = "1"
tabled = "0.10"
rand = "0.8.5"
serde_json = "1"
[dev-dependencies]
ctor = "0.2"

View file

@ -216,9 +216,9 @@ dependencies = [
"fxhash",
"itertools 0.12.1",
"loro 0.16.2",
"loro 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9)",
"loro-common 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9)",
"loro 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a)",
"rand",
"serde_json",
"tabled",
"tracing",
]
@ -436,13 +436,13 @@ dependencies = [
[[package]]
name = "loro"
version = "0.16.2"
source = "git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9#83938290ab2666d85c0c72169127611585a05cf9"
source = "git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a#39caa6cedf66d8f6c77f0886e70ec1270a96bd1a"
dependencies = [
"either",
"enum-as-inner 0.6.0",
"generic-btree",
"loro-delta 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9)",
"loro-internal 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9)",
"loro-delta 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a)",
"loro-internal 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a)",
"tracing",
]
@ -464,12 +464,12 @@ dependencies = [
[[package]]
name = "loro-common"
version = "0.16.2"
source = "git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9#83938290ab2666d85c0c72169127611585a05cf9"
source = "git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a#39caa6cedf66d8f6c77f0886e70ec1270a96bd1a"
dependencies = [
"arbitrary",
"enum-as-inner 0.6.0",
"fxhash",
"loro-rle 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9)",
"loro-rle 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a)",
"nonmax",
"serde",
"serde_columnar",
@ -491,7 +491,7 @@ dependencies = [
[[package]]
name = "loro-delta"
version = "0.16.2"
source = "git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9#83938290ab2666d85c0c72169127611585a05cf9"
source = "git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a#39caa6cedf66d8f6c77f0886e70ec1270a96bd1a"
dependencies = [
"arrayvec",
"enum-as-inner 0.5.1",
@ -537,7 +537,7 @@ dependencies = [
[[package]]
name = "loro-internal"
version = "0.16.2"
source = "git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9#83938290ab2666d85c0c72169127611585a05cf9"
source = "git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a#39caa6cedf66d8f6c77f0886e70ec1270a96bd1a"
dependencies = [
"append-only-bytes",
"arref",
@ -550,10 +550,10 @@ dependencies = [
"im",
"itertools 0.12.1",
"leb128",
"loro-common 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9)",
"loro-delta 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9)",
"loro-rle 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9)",
"loro_fractional_index 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9)",
"loro-common 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a)",
"loro-delta 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a)",
"loro-rle 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a)",
"loro_fractional_index 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a)",
"md5",
"num",
"num-derive",
@ -584,7 +584,7 @@ dependencies = [
[[package]]
name = "loro-rle"
version = "0.16.2"
source = "git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9#83938290ab2666d85c0c72169127611585a05cf9"
source = "git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a#39caa6cedf66d8f6c77f0886e70ec1270a96bd1a"
dependencies = [
"append-only-bytes",
"arref",
@ -613,7 +613,7 @@ dependencies = [
[[package]]
name = "loro_fractional_index"
version = "0.16.2"
source = "git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9#83938290ab2666d85c0c72169127611585a05cf9"
source = "git+https://github.com/loro-dev/loro.git?rev=39caa6cedf66d8f6c77f0886e70ec1270a96bd1a#39caa6cedf66d8f6c77f0886e70ec1270a96bd1a"
dependencies = [
"imbl",
"rand",

View file

@ -1,11 +1,13 @@
use std::{
collections::VecDeque,
fmt::{Debug, Formatter},
sync::{Arc, Mutex},
};
use enum_as_inner::EnumAsInner;
use enum_dispatch::enum_dispatch;
use fxhash::FxHashMap;
use fxhash::{FxHashMap, FxHashSet};
use itertools::Itertools;
use loro::{
Container, ContainerID, ContainerType, Frontiers, LoroDoc, LoroValue, PeerID, UndoManager, ID,
};
@ -52,7 +54,7 @@ impl Actor {
info_span!("ApplyDiff", id = id).in_scope(|| {
let mut tracker = cb_tracker.lock().unwrap();
tracker.apply_diff(e)
})
});
}));
let mut default_history = FxHashMap::default();
default_history.insert(Vec::new(), loro.get_deep_value());
@ -123,28 +125,20 @@ impl Actor {
pub fn undo(&mut self, undo_length: u32) {
self.loro.attach();
let mut before_undo = self.loro.get_deep_value();
let before_undo = self.loro.get_deep_value();
// println!("\n\nstart undo\n");
for _ in 0..undo_length {
self.undo_manager.undo.undo(&self.loro).unwrap();
}
// println!("\n\nstart redo\n");
for _ in 0..undo_length {
self.undo_manager.undo.redo(&self.loro).unwrap();
}
let mut after_undo = self.loro.get_deep_value();
Self::patch_tree_undo_position(&mut before_undo);
Self::patch_tree_undo_position(&mut after_undo);
assert_value_eq(&before_undo, &after_undo);
}
fn patch_tree_undo_position(a: &mut LoroValue) {
let root = Arc::make_mut(a.as_map_mut().unwrap());
let tree = root.get_mut("tree").unwrap();
let nodes = Arc::make_mut(tree.as_list_mut().unwrap());
for node in nodes.iter_mut() {
let node = Arc::make_mut(node.as_map_mut().unwrap());
node.remove("position");
}
let after_undo = self.loro.get_deep_value();
assert_value_eq(&before_undo, &after_undo);
}
pub fn check_tracker(&self) {
@ -347,6 +341,47 @@ pub trait ActorTrait {
}
pub fn assert_value_eq(a: &LoroValue, b: &LoroValue) {
fn eq_without_position(a: &LoroValue, b: &LoroValue) -> bool {
match (a, b) {
(LoroValue::Map(a), LoroValue::Map(b)) => {
for (k, v) in a.iter() {
if k == "position" {
continue;
}
if !eq_without_position(v, b.get(k).unwrap_or(&LoroValue::I64(0))) {
return false;
}
}
for (k, v) in b.iter() {
if k == "position" {
continue;
}
if !eq_without_position(v, a.get(k).unwrap_or(&LoroValue::I64(0))) {
return false;
}
}
true
}
(LoroValue::List(a), LoroValue::List(b)) => {
if a.len() != b.len() {
return false;
}
if is_tree_values(a.as_ref()) {
assert_tree_value_eq(a, b);
true
} else {
a.iter()
.zip(b.iter())
.all(|(a, b)| eq_without_position(a, b))
}
}
(a, b) => a == b,
}
}
#[must_use]
fn eq(a: &LoroValue, b: &LoroValue) -> bool {
match (a, b) {
@ -385,7 +420,15 @@ pub fn assert_value_eq(a: &LoroValue, b: &LoroValue) {
true
}
(a, b) => a == b,
(LoroValue::List(a_list), LoroValue::List(b_list)) => {
if is_tree_values(a_list.as_ref()) {
assert_tree_value_eq(a_list, b_list);
true
} else {
eq_without_position(a, b)
}
}
(a, b) => eq_without_position(a, b),
}
}
assert!(
@ -395,3 +438,154 @@ pub fn assert_value_eq(a: &LoroValue, b: &LoroValue) {
b
);
}
pub fn is_tree_values(value: &[LoroValue]) -> bool {
if let Some(LoroValue::Map(map)) = value.first() {
let map_keys = map.as_ref().keys().cloned().collect::<FxHashSet<_>>();
return map_keys.contains("id")
&& map_keys.contains("parent")
&& map_keys.contains("meta")
&& map_keys.contains("position");
}
false
}
#[derive(Debug, Clone)]
struct Node {
children: Vec<Node>,
meta: FxHashMap<String, LoroValue>,
position: String,
}
struct FlatNode {
id: String,
parent: Option<String>,
meta: FxHashMap<String, LoroValue>,
index: usize,
position: String,
}
impl FlatNode {
fn from_loro_value(value: &LoroValue) -> Self {
let map = value.as_map().unwrap();
let id = map.get("id").unwrap().as_string().unwrap().to_string();
let parent = map
.get("parent")
.unwrap()
.as_string()
.map_or(None, |x| Some(x.to_string()));
let meta = map.get("meta").unwrap().as_map().unwrap().as_ref().clone();
let index = *map.get("index").unwrap().as_i64().unwrap() as usize;
let position = map
.get("position")
.unwrap()
.as_string()
.unwrap()
.to_string();
FlatNode {
id,
parent,
meta,
index,
position,
}
}
}
impl Node {
fn from_loro_value(value: &[LoroValue]) -> Vec<Self> {
let mut node_map = FxHashMap::default();
let mut parent_child_map = FxHashMap::default();
// 首先将所有扁平节点转换为TreeNode并存储在HashMap中以便快速查找
for flat_node in value.iter() {
let flat_node = FlatNode::from_loro_value(flat_node);
let tree_node = Node {
// id: flat_node.id.clone(),
// parent: flat_node.parent.clone(),
children: vec![],
meta: flat_node.meta,
// index: flat_node.index,
position: flat_node.position,
};
node_map.insert(flat_node.id.clone(), tree_node);
parent_child_map
.entry(flat_node.parent)
.or_insert_with(Vec::new)
.push((flat_node.index, flat_node.id));
}
let mut node_map_clone = node_map.clone();
for (parent_id, child_ids) in parent_child_map.iter() {
if let Some(parent_id) = parent_id {
if let Some(parent_node) = node_map.get_mut(parent_id) {
for (_, child_id) in child_ids.into_iter().sorted_by_key(|x| x.0) {
if let Some(child_node) = node_map_clone.remove(child_id) {
parent_node.children.push(child_node);
}
}
}
}
}
parent_child_map.get(&None).map_or(vec![], |root_ids| {
root_ids
.iter()
.filter_map(|(_i, id)| node_map.remove(id))
.collect::<Vec<_>>()
})
}
}
pub fn assert_tree_value_eq(a: &[LoroValue], b: &[LoroValue]) {
let a_tree = Node::from_loro_value(a);
let b_tree = Node::from_loro_value(b);
let mut a_q = VecDeque::from_iter([a_tree]);
let mut b_q = VecDeque::from_iter([b_tree]);
while let (Some(a_node), Some(b_node)) = (a_q.pop_front(), b_q.pop_front()) {
let mut children_a = vec![];
let mut children_b = vec![];
let a_meta = a_node
.into_iter()
.map(|x| {
children_a.extend(x.children);
let mut meta = x
.meta
.into_iter()
.sorted_by_key(|(k, _)| k.clone())
.map(|(mut k, v)| {
k.push_str(v.as_string().map_or("", |f| f.as_str()));
k
})
.collect::<String>();
meta.push_str(&x.position);
meta
})
.collect::<FxHashSet<_>>();
let b_meta = b_node
.into_iter()
.map(|x| {
children_b.extend(x.children);
let mut meta = x
.meta
.into_iter()
.sorted_by_key(|(k, _)| k.clone())
.map(|(mut k, v)| {
k.push_str(v.as_string().map_or("", |f| f.as_str()));
k
})
.collect::<String>();
meta.push_str(&x.position);
meta
})
.collect::<FxHashSet<_>>();
assert!(a_meta.difference(&b_meta).count() == 0);
assert_eq!(children_a.len(), children_b.len());
if children_a.is_empty() {
continue;
}
a_q.push_back(children_a);
b_q.push_back(children_b);
}
}

View file

@ -168,11 +168,11 @@ impl Actionable for TreeAction {
*target = (id.peer, id.counter);
}
TreeActionInner::Delete => {
let target_index = target.1 as usize % node_num;
let target_index = target.0 as usize % node_num;
*target = (nodes[target_index].peer, nodes[target_index].counter);
}
TreeActionInner::Move { parent, index } => {
let target_index = target.1 as usize % node_num;
let target_index = target.0 as usize % node_num;
*target = (nodes[target_index].peer, nodes[target_index].counter);
let mut parent_idx = parent.0 as usize % node_num;
while target_index == parent_idx {
@ -202,7 +202,7 @@ impl Actionable for TreeAction {
*c = nodes[other_idx].counter;
}
TreeActionInner::Meta { meta: (_, v) } => {
let target_index = target.1 as usize % node_num;
let target_index = target.0 as usize % node_num;
*target = (nodes[target_index].peer, nodes[target_index].counter);
if matches!(v, FuzzValue::Container(_)) {
*v = FuzzValue::I32(0);

View file

@ -1,4 +1,7 @@
use std::fmt::{Debug, Display};
use std::{
fmt::{Debug, Display},
time::Instant,
};
use arbitrary::Arbitrary;
use fxhash::FxHashSet;
@ -241,9 +244,10 @@ impl CRDTFuzzer {
}
fn check_history(&mut self) {
for actor in self.actors.iter_mut() {
actor.check_history();
}
self.actors[0].check_history();
// for actor in self.actors.iter_mut() {
// actor.check_history();
// }
}
fn site_num(&self) -> usize {
@ -302,16 +306,177 @@ pub fn test_multi_sites(site_num: u8, fuzz_targets: Vec<FuzzTarget>, actions: &m
let mut applied = Vec::new();
for action in actions.iter_mut() {
fuzzer.pre_process(action);
info_span!("ApplyAction", ?action).in_scope(|| {
applied.push(action.clone());
info!("OptionsTable \n{}", (&applied).table());
fuzzer.apply_action(action);
});
}
let span = &info_span!("check synced");
let _g = span.enter();
fuzzer.check_equal();
fuzzer.check_tracker();
fuzzer.check_history();
}
pub fn minify_error<T, F, N>(site_num: u8, f: F, normalize: N, actions: Vec<T>)
where
F: Fn(u8, &mut [T]),
N: Fn(u8, &mut [T]) -> Vec<T>,
T: Clone + Debug,
{
std::panic::set_hook(Box::new(|_info| {
// ignore panic output
// println!("{:?}", _info);
}));
let f_ref: *const _ = &f;
let f_ref: usize = f_ref as usize;
#[allow(clippy::redundant_clone)]
let mut actions_clone = actions.clone();
let action_ref: usize = (&mut actions_clone) as *mut _ as usize;
#[allow(clippy::blocks_in_conditions)]
if std::panic::catch_unwind(|| {
// SAFETY: test
let f = unsafe { &*(f_ref as *const F) };
// SAFETY: test
let actions_ref = unsafe { &mut *(action_ref as *mut Vec<T>) };
f(site_num, actions_ref);
})
.is_ok()
{
println!("No Error Found");
return;
}
let mut minified = actions.clone();
let mut candidates = Vec::new();
println!("Setup candidates...");
for i in 0..actions.len() {
let mut new = actions.clone();
new.remove(i);
candidates.push(new);
}
println!("Minifying...");
let start = Instant::now();
while let Some(candidate) = candidates.pop() {
let f_ref: *const _ = &f;
let f_ref: usize = f_ref as usize;
let mut actions_clone = candidate.clone();
let action_ref: usize = (&mut actions_clone) as *mut _ as usize;
#[allow(clippy::blocks_in_conditions)]
if std::panic::catch_unwind(|| {
// SAFETY: test
let f = unsafe { &*(f_ref as *const F) };
// SAFETY: test
let actions_ref = unsafe { &mut *(action_ref as *mut Vec<T>) };
f(site_num, actions_ref);
})
.is_err()
{
for i in 0..candidate.len() {
let mut new = candidate.clone();
new.remove(i);
candidates.push(new);
}
if candidate.len() < minified.len() {
minified = candidate;
println!("New min len={}", minified.len());
}
if candidates.len() > 40 {
candidates.drain(0..30);
}
}
if start.elapsed().as_secs() > 10 && minified.len() <= 4 {
break;
}
if start.elapsed().as_secs() > 60 {
break;
}
}
let minified = normalize(site_num, &mut minified);
println!(
"Old Length {}, New Length {}",
actions.len(),
minified.len()
);
dbg!(&minified);
if actions.len() > minified.len() {
minify_error(site_num, f, normalize, minified);
}
}
pub fn minify_simple<T, F, N>(site_num: u8, f: F, normalize: N, actions: Vec<T>)
where
F: Fn(u8, &mut [T]),
N: Fn(u8, &mut [T]) -> Vec<T>,
T: Clone + Debug,
{
std::panic::set_hook(Box::new(|_info| {
// ignore panic output
// println!("{:?}", _info);
}));
let f_ref: *const _ = &f;
let f_ref: usize = f_ref as usize;
#[allow(clippy::redundant_clone)]
let mut actions_clone = actions.clone();
let action_ref: usize = (&mut actions_clone) as *mut _ as usize;
#[allow(clippy::blocks_in_conditions)]
if std::panic::catch_unwind(|| {
// SAFETY: test
let f = unsafe { &*(f_ref as *const F) };
// SAFETY: test
let actions_ref = unsafe { &mut *(action_ref as *mut Vec<T>) };
f(site_num, actions_ref);
})
.is_ok()
{
println!("No Error Found");
return;
}
let mut minified = actions.clone();
let mut current_index = minified.len() as i64 - 1;
while current_index > 0 {
let a = minified.remove(current_index as usize);
let f_ref: *const _ = &f;
let f_ref: usize = f_ref as usize;
let mut actions_clone = minified.clone();
let action_ref: usize = (&mut actions_clone) as *mut _ as usize;
let mut re = false;
#[allow(clippy::blocks_in_conditions)]
if std::panic::catch_unwind(|| {
// SAFETY: test
let f = unsafe { &*(f_ref as *const F) };
// SAFETY: test
let actions_ref = unsafe { &mut *(action_ref as *mut Vec<T>) };
f(site_num, actions_ref);
})
.is_err()
{
re = true;
} else {
minified.insert(current_index as usize, a);
}
println!(
"{}/{} {}",
actions.len() as i64 - current_index,
actions.len(),
re
);
current_index -= 1;
}
let minified = normalize(site_num, &mut minified);
println!("{:?}", &minified);
println!(
"Old Length {}, New Length {}",
actions.len(),
minified.len()
);
if actions.len() > minified.len() {
minify_simple(site_num, f, normalize, minified);
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,4 @@
use serde_columnar::ColumnarError;
use thiserror::Error;
@ -32,8 +33,12 @@ pub enum LoroError {
NotFoundError(Box<str>),
#[error("Transaction error ({0})")]
TransactionError(Box<str>),
#[error("Index out of bound. The given pos is {pos}, but the length is {len}")]
OutOfBound { pos: usize, len: usize },
#[error("Index out of bound. The given pos is {pos}, but the length is {len}. {info}")]
OutOfBound {
pos: usize,
len: usize,
info: Box<str>,
},
#[error("Every op id should be unique. ID {id} has been used. You should use a new PeerID to edit the content. ")]
UsedOpID { id: ID },
#[error("Movable Tree Error: {0}")]

View file

@ -61,20 +61,14 @@ mod tree {
let mut versions = vec![];
let size = 1000;
for _ in 0..size {
ids.push(
loro.with_txn(|txn| tree.create_with_txn(txn, None, 0))
.unwrap(),
)
ids.push(tree.create(None).unwrap())
}
let mut rng: StdRng = rand::SeedableRng::seed_from_u64(0);
let mut n = 1000;
while n > 0 {
let i = rng.gen::<usize>() % size;
let j = rng.gen::<usize>() % size;
if loro
.with_txn(|txn| tree.mov_with_txn(txn, ids[i], ids[j], 0))
.is_ok()
{
if tree.mov(ids[i], ids[j]).is_ok() {
versions.push(loro.oplog_frontiers());
n -= 1;
};
@ -94,15 +88,11 @@ mod tree {
let tree = loro.get_tree("tree");
let mut ids = vec![];
let mut versions = vec![];
let id1 = loro
.with_txn(|txn| tree.create_with_txn(txn, None, 0))
.unwrap();
let id1 = tree.create(None).unwrap();
ids.push(id1);
versions.push(loro.oplog_frontiers());
for _ in 1..depth {
let id = loro
.with_txn(|txn| tree.create_with_txn(txn, *ids.last().unwrap(), 0))
.unwrap();
let id = tree.create(*ids.last().unwrap()).unwrap();
ids.push(id);
versions.push(loro.oplog_frontiers());
}
@ -124,11 +114,7 @@ mod tree {
let mut ids = vec![];
let size = 1000;
for _ in 0..size {
ids.push(
doc_a
.with_txn(|txn| tree_a.create_with_txn(txn, None, 0))
.unwrap(),
)
ids.push(tree_a.create(None).unwrap())
}
doc_b.import(&doc_a.export_snapshot()).unwrap();
let mut rng: StdRng = rand::SeedableRng::seed_from_u64(0);
@ -138,16 +124,10 @@ mod tree {
let i = rng.gen::<usize>() % size;
let j = rng.gen::<usize>() % size;
if t % 2 == 0 {
let mut txn = doc_a.txn().unwrap();
tree_a
.mov_with_txn(&mut txn, ids[i], ids[j], 0)
.unwrap_or_default();
tree_a.mov(ids[i], ids[j]).unwrap_or_default();
doc_b.import(&doc_a.export_from(&doc_b.oplog_vv())).unwrap();
} else {
let mut txn = doc_b.txn().unwrap();
tree_b
.mov_with_txn(&mut txn, ids[i], ids[j], 0)
.unwrap_or_default();
tree_b.mov(ids[i], ids[j]).unwrap_or_default();
doc_a.import(&doc_b.export_from(&doc_a.oplog_vv())).unwrap();
}
}

View file

@ -6,19 +6,15 @@ use rand::{rngs::StdRng, Rng};
#[allow(unused)]
fn checkout() {
let depth = 300;
let loro = LoroDoc::default();
let loro = LoroDoc::new_auto_commit();
let tree = loro.get_tree("tree");
let mut ids = vec![];
let mut versions = vec![];
let id1 = loro
.with_txn(|txn| tree.create_with_txn(txn, None, 0))
.unwrap();
let id1 = tree.create_at(None, 0).unwrap();
ids.push(id1);
versions.push(loro.oplog_frontiers());
for _ in 1..depth {
let id = loro
.with_txn(|txn| tree.create_with_txn(txn, *ids.last().unwrap(), 0))
.unwrap();
let id = tree.create_at(*ids.last().unwrap(), 0).unwrap();
ids.push(id);
versions.push(loro.oplog_frontiers());
}
@ -63,8 +59,7 @@ fn create() {
let loro = LoroDoc::default();
let tree = loro.get_tree("tree");
for _ in 0..size {
loro.with_txn(|txn| tree.create_with_txn(txn, None, 0))
.unwrap();
tree.create_at(None, 0).unwrap();
}
println!("encode snapshot size {:?}\n", loro.export_snapshot().len());
println!(

View file

@ -6,6 +6,10 @@
//!
use crate::{arena::SharedArena, InternalString, ID};
pub mod list;
pub mod map;
pub mod richtext;
pub mod tree;
pub mod idx {
use super::super::ContainerType;
@ -86,11 +90,6 @@ pub mod idx {
}
}
}
pub mod list;
pub mod map;
pub mod richtext;
pub mod tree;
use idx::ContainerIdx;
pub use loro_common::ContainerType;

View file

@ -1,5 +1,5 @@
use fractional_index::FractionalIndex;
use fxhash::FxHashMap;
use fxhash::{FxHashMap, FxHashSet};
use itertools::Itertools;
use loro_common::{IdFull, TreeID};
use std::fmt::Debug;
@ -29,19 +29,14 @@ pub enum TreeExternalDiff {
parent: Option<TreeID>,
index: usize,
position: FractionalIndex,
old_parent: TreeParentId,
old_index: usize,
},
Delete {
old_parent: TreeParentId,
old_index: usize,
},
Delete,
}
impl TreeDiff {
pub(crate) fn compose(mut self, other: Self) -> Self {
// TODO: better compose
self.diff.extend(other.diff);
// self = compose_tree_diff(&self);
self
}
@ -50,125 +45,38 @@ impl TreeDiff {
self
}
fn to_hash_map_mut(&mut self) -> FxHashMap<TreeID, usize> {
let mut ans = FxHashSet::default();
for index in (0..self.diff.len()).rev() {
let target = self.diff[index].target;
if ans.contains(&target) {
self.diff.remove(index);
continue;
}
ans.insert(target);
}
self.iter()
.map(|x| x.target)
.enumerate()
.map(|(i, x)| (x, i))
.collect()
}
pub(crate) fn transform(&mut self, b: &TreeDiff, left_prior: bool) {
// println!("\ntransform prior {:?} {:?} \nb {:?}", left_prior, self, b);
if b.is_empty() || self.is_empty() {
return;
}
let b_update: FxHashMap<_, _> = b.diff.iter().map(|d| (d.target, &d.action)).collect();
let mut self_update: FxHashMap<_, _> = self
.diff
.iter()
.enumerate()
.map(|(i, d)| (d.target, (&d.action, i)))
.collect();
let mut removes = Vec::new();
for (target, diff) in b_update {
if self_update.contains_key(&target) && diff == self_update.get(&target).unwrap().0 {
let (_, i) = self_update.remove(&target).unwrap();
removes.push(i);
continue;
}
if !left_prior {
if let Some((_, i)) = self_update.remove(&target) {
removes.push(i);
}
}
}
for i in removes.into_iter().sorted().rev() {
self.diff.remove(i);
}
let mut b_parent = FxHashMap::default();
fn reset_index(
b_parent: &FxHashMap<TreeParentId, Vec<i32>>,
index: &mut usize,
parent: &TreeParentId,
left_priority: bool,
) {
if let Some(b_indices) = b_parent.get(parent) {
for i in b_indices.iter() {
if (i.unsigned_abs() as usize) < *index
|| (i.unsigned_abs() as usize == *index && !left_priority)
{
if i > &0 {
*index += 1;
} else if *index > (i.unsigned_abs() as usize) {
*index = index.saturating_sub(1);
}
} else {
break;
}
}
}
}
for diff in b.diff.iter() {
match &diff.action {
TreeExternalDiff::Create {
parent,
index,
position: _,
} => {
b_parent
.entry(TreeParentId::from(*parent))
.or_insert_with(Vec::new)
.push(*index as i32);
}
TreeExternalDiff::Move {
parent,
index,
position: _,
old_parent,
old_index,
} => {
b_parent
.entry(*old_parent)
.or_insert_with(Vec::new)
.push(-(*old_index as i32));
b_parent
.entry(TreeParentId::from(*parent))
.or_insert_with(Vec::new)
.push(*index as i32);
}
TreeExternalDiff::Delete {
old_index,
old_parent,
} => {
b_parent
.entry(*old_parent)
.or_insert_with(Vec::new)
.push(-(*old_index as i32));
}
}
}
b_parent
.iter_mut()
.for_each(|(_, v)| v.sort_unstable_by_key(|i| i.abs()));
for diff in self.iter_mut() {
match &mut diff.action {
TreeExternalDiff::Create {
parent,
index,
position: _,
} => reset_index(&b_parent, index, &TreeParentId::from(*parent), left_prior),
TreeExternalDiff::Move {
parent,
index,
position: _,
old_parent,
old_index,
} => {
reset_index(&b_parent, index, &TreeParentId::from(*parent), left_prior);
reset_index(&b_parent, old_index, old_parent, left_prior);
}
TreeExternalDiff::Delete {
old_index,
old_parent,
} => {
reset_index(&b_parent, old_index, old_parent, left_prior);
}
if !left_prior {
let mut self_update = self.to_hash_map_mut();
for i in b
.iter()
.map(|x| x.target)
.filter_map(|x| self_update.remove(&x))
.sorted()
.rev()
{
self.remove(i);
}
}
}
@ -239,7 +147,6 @@ impl TreeDeltaItem {
is_old_parent_deleted: bool,
position: Option<FractionalIndex>,
) -> Self {
// TODO: check op id
let action = if matches!(parent, TreeParentId::Unexist) {
TreeInternalDiff::UnCreate
} else {

View file

@ -50,7 +50,6 @@ impl DiffCalculatorTrait for TreeDiffCalculator {
on_new_container(&d.target.associated_meta_container())
}
});
tracing::info!("\ndiff {:?}", diff);
InternalDiff::Tree(diff)
@ -70,7 +69,6 @@ impl TreeDiffCalculator {
fn checkout(&mut self, to: &VersionVector, oplog: &OpLog) {
let tree_ops = oplog.op_groups.get_tree(&self.container).unwrap();
let mut tree_cache = tree_ops.tree_for_diff.lock().unwrap();
let s = format!("checkout current {:?} to {:?}", &tree_cache.current_vv, &to);
let s = tracing::span!(tracing::Level::INFO, "checkout", s = s);
let _e = s.enter();
@ -451,7 +449,7 @@ impl TreeCacheForDiff {
ans.push((*tree_id, op.position.clone(), op.id_full()));
}
}
ans.sort_by(|a, b| a.1.cmp(&b.1));
ans
}
}

View file

@ -216,6 +216,14 @@ impl DiffVariant {
(a, _) => Err(a),
}
}
pub fn is_empty(&self) -> bool {
match self {
DiffVariant::Internal(diff) => diff.is_empty(),
DiffVariant::External(diff) => diff.is_empty(),
DiffVariant::None => true,
}
}
}
#[non_exhaustive]

View file

@ -16,19 +16,22 @@ use crate::{
};
use append_only_bytes::BytesSlice;
use enum_as_inner::EnumAsInner;
use fxhash::FxHashMap;
use fxhash::{FxHashMap, FxHashSet};
use generic_btree::rle::HasLength;
use loro_common::{
ContainerID, ContainerType, IdFull, InternalString, LoroError, LoroResult, LoroValue, ID,
ContainerID, ContainerType, IdFull, InternalString, LoroError, LoroResult, LoroValue, TreeID,
ID,
};
use serde::{Deserialize, Serialize};
use std::{
borrow::Cow,
cmp::Reverse,
collections::BinaryHeap,
fmt::Debug,
ops::Deref,
sync::{Arc, Mutex, Weak},
};
use tracing::{error, info, instrument};
use tracing::{debug, error, info, instrument, trace};
mod tree;
pub use tree::TreeHandler;
@ -1060,8 +1063,11 @@ impl Handler {
pub(crate) fn apply_diff(
&self,
diff: Diff,
on_container_remap: &mut dyn FnMut(ContainerID, ContainerID),
container_remap: &mut FxHashMap<ContainerID, ContainerID>,
) -> LoroResult<()> {
let on_container_remap = &mut |old_id, new_id| {
container_remap.insert(old_id, new_id);
};
match self {
Self::Map(x) => {
let diff = diff.into_map().unwrap();
@ -1098,20 +1104,60 @@ impl Handler {
x.apply_delta(delta, on_container_remap)?;
}
Self::Tree(x) => {
fn remap_tree_id(
id: &mut TreeID,
container_remap: &FxHashMap<ContainerID, ContainerID>,
) {
let mut remapped = false;
let mut map_id = id.associated_meta_container();
while let Some(rid) = container_remap.get(&map_id) {
remapped = true;
map_id = rid.clone();
}
if remapped {
*id = TreeID::new(
*map_id.as_normal().unwrap().0,
*map_id.as_normal().unwrap().1,
)
}
}
for diff in diff.into_tree().unwrap().diff {
let target = diff.target;
let mut target = diff.target;
match diff.action {
TreeExternalDiff::Create {
parent,
index,
position: _,
mut parent,
index: _,
position,
} => {
x.create_at_with_target(parent, index, target)?;
// create map event
let new_target = x.__internal__next_tree_id();
if let Some(p) = parent.as_mut() {
remap_tree_id(p, container_remap)
}
if x.create_at_with_target_for_apply_diff(parent, position, new_target)?
{
container_remap.insert(
target.associated_meta_container(),
new_target.associated_meta_container(),
);
}
}
TreeExternalDiff::Delete { .. } => x.delete(target)?,
TreeExternalDiff::Move { parent, index, .. } => {
x.move_to(target, parent, index)?
TreeExternalDiff::Move {
mut parent,
index: _,
position,
} => {
if let Some(p) = parent.as_mut() {
remap_tree_id(p, container_remap)
}
remap_tree_id(&mut target, container_remap);
x.move_at_with_target_for_apply_diff(parent, position, target)?;
}
TreeExternalDiff::Delete => {
remap_tree_id(&mut target, container_remap);
// println!("delete {:?}", target);
if x.contains(target) {
x.delete(target)?
}
}
}
}
@ -1329,6 +1375,7 @@ impl TextHandler {
return Err(LoroError::OutOfBound {
pos,
len: self.len_event(),
info: format!("Position: {}:{}", file!(), line!()).into_boxed_str(),
});
}
@ -1426,6 +1473,7 @@ impl TextHandler {
return Err(LoroError::OutOfBound {
pos: pos + len,
len: self.len_event(),
info: format!("Position: {}:{}", file!(), line!()).into_boxed_str(),
});
}
@ -1503,7 +1551,11 @@ impl TextHandler {
));
}
if end > len {
return Err(LoroError::OutOfBound { pos: end, len });
return Err(LoroError::OutOfBound {
pos: end,
len,
info: format!("Position: {}:{}", file!(), line!()).into_boxed_str(),
});
}
let (entity_range, styles) =
state.get_entity_range_and_text_styles_at_range(start..end, PosType::Event);
@ -1579,7 +1631,11 @@ impl TextHandler {
let len = self.len_event();
if end > len {
return Err(LoroError::OutOfBound { pos: end, len });
return Err(LoroError::OutOfBound {
pos: end,
len,
info: format!("Position: {}:{}", file!(), line!()).into_boxed_str(),
});
}
let inner = self.inner.try_attached_state()?;
@ -1873,6 +1929,7 @@ impl ListHandler {
if pos > self.len() {
return Err(LoroError::OutOfBound {
pos,
info: format!("Position: {}:{}", file!(), line!()).into_boxed_str(),
len: self.len(),
});
}
@ -1957,6 +2014,7 @@ impl ListHandler {
if pos > self.len() {
return Err(LoroError::OutOfBound {
pos,
info: format!("Position: {}:{}", file!(), line!()).into_boxed_str(),
len: self.len(),
});
}
@ -1997,6 +2055,7 @@ impl ListHandler {
if pos + len > self.len() {
return Err(LoroError::OutOfBound {
pos: pos + len,
info: format!("Position: {}:{}", file!(), line!()).into_boxed_str(),
len: self.len(),
});
}
@ -2031,6 +2090,7 @@ impl ListHandler {
let list = l.try_lock().unwrap();
let value = list.value.get(index).ok_or(LoroError::OutOfBound {
pos: index,
info: format!("Position: {}:{}", file!(), line!()).into_boxed_str(),
len: list.value.len(),
})?;
match value {
@ -2050,6 +2110,7 @@ impl ListHandler {
}) else {
return Err(LoroError::OutOfBound {
pos: index,
info: format!("Position: {}:{}", file!(), line!()).into_boxed_str(),
len: a.with_state(|state| state.as_list_state().unwrap().len()),
});
};
@ -2249,6 +2310,7 @@ impl MovableListHandler {
if pos > d.value.len() {
return Err(LoroError::OutOfBound {
pos,
info: format!("Position: {}:{}", file!(), line!()).into_boxed_str(),
len: d.value.len(),
});
}
@ -2271,6 +2333,7 @@ impl MovableListHandler {
if pos > self.len() {
return Err(LoroError::OutOfBound {
pos,
info: format!("Position: {}:{}", file!(), line!()).into_boxed_str(),
len: self.len(),
});
}
@ -2310,12 +2373,14 @@ impl MovableListHandler {
if from >= d.value.len() {
return Err(LoroError::OutOfBound {
pos: from,
info: format!("Position: {}:{}", file!(), line!()).into_boxed_str(),
len: d.value.len(),
});
}
if to >= d.value.len() {
return Err(LoroError::OutOfBound {
pos: to,
info: format!("Position: {}:{}", file!(), line!()).into_boxed_str(),
len: d.value.len(),
});
}
@ -2337,6 +2402,7 @@ impl MovableListHandler {
if from >= self.len() {
return Err(LoroError::OutOfBound {
pos: from,
info: format!("Position: {}:{}", file!(), line!()).into_boxed_str(),
len: self.len(),
});
}
@ -2344,6 +2410,7 @@ impl MovableListHandler {
if to >= self.len() {
return Err(LoroError::OutOfBound {
pos: to,
info: format!("Position: {}:{}", file!(), line!()).into_boxed_str(),
len: self.len(),
});
}
@ -2439,6 +2506,7 @@ impl MovableListHandler {
if pos > d.value.len() {
return Err(LoroError::OutOfBound {
pos,
info: format!("Position: {}:{}", file!(), line!()).into_boxed_str(),
len: d.value.len(),
});
}
@ -2461,6 +2529,7 @@ impl MovableListHandler {
if pos > self.len() {
return Err(LoroError::OutOfBound {
pos,
info: format!("Position: {}:{}", file!(), line!()).into_boxed_str(),
len: self.len(),
});
}
@ -2495,6 +2564,7 @@ impl MovableListHandler {
if index >= d.value.len() {
return Err(LoroError::OutOfBound {
pos: index,
info: format!("Position: {}:{}", file!(), line!()).into_boxed_str(),
len: d.value.len(),
});
}
@ -2516,6 +2586,7 @@ impl MovableListHandler {
if index >= self.len() {
return Err(LoroError::OutOfBound {
pos: index,
info: format!("Position: {}:{}", file!(), line!()).into_boxed_str(),
len: self.len(),
});
}
@ -2604,6 +2675,7 @@ impl MovableListHandler {
if pos + len > self.len() {
return Err(LoroError::OutOfBound {
pos: pos + len,
info: format!("Position: {}:{}", file!(), line!()).into_boxed_str(),
len: self.len(),
});
}
@ -2651,6 +2723,7 @@ impl MovableListHandler {
let list = l.try_lock().unwrap();
let value = list.value.get(index).ok_or(LoroError::OutOfBound {
pos: index,
info: format!("Position: {}:{}", file!(), line!()).into_boxed_str(),
len: list.value.len(),
})?;
match value {
@ -2675,6 +2748,7 @@ impl MovableListHandler {
}) else {
return Err(LoroError::OutOfBound {
pos: index,
info: format!("Position: {}:{}", file!(), line!()).into_boxed_str(),
len: a.with_state(|state| state.as_list_state().unwrap().len()),
});
};
@ -2872,7 +2946,54 @@ impl MovableListHandler {
unimplemented!();
}
MaybeDetached::Attached(_) => {
debug!("movable list apply_delta {:#?}", &delta);
// preprocess all deletions. They will be used to infer the move ops
let mut index = 0;
let mut to_delete = FxHashMap::default();
for d in delta.iter() {
match d {
loro_delta::DeltaItem::Retain { len, .. } => {
index += len;
}
loro_delta::DeltaItem::Replace { delete, .. } => {
if *delete > 0 {
for i in index..index + *delete {
if let Some(LoroValue::Container(c)) = self.get(i) {
to_delete.insert(c, i);
}
}
index += *delete;
}
}
}
}
fn update_on_insert(
d: &mut FxHashMap<ContainerID, usize>,
index: usize,
len: usize,
) {
for pos in d.values_mut() {
if *pos >= index {
*pos += len;
}
}
}
fn update_on_delete(d: &mut FxHashMap<ContainerID, usize>, index: usize) {
for pos in d.values_mut() {
if *pos >= index {
*pos -= 1;
}
}
}
// process all insertions and moves
let mut index = 0;
let mut deleted = Vec::new();
let mut next_deleted = BinaryHeap::new();
let mut index_shift = 0;
for d in delta.iter() {
match d {
loro_delta::DeltaItem::Retain { len, .. } => {
@ -2881,28 +3002,93 @@ impl MovableListHandler {
loro_delta::DeltaItem::Replace {
value,
delete,
attr: _attr,
attr,
} => {
// TODO: handle move error
self.delete(index, *delete)?;
if *delete > 0 {
// skip the deletion if it is already processed by moving
let mut d = *delete;
while let Some(Reverse(old_index)) = next_deleted.peek() {
if *old_index + index_shift < index + d {
assert!(index <= *old_index + index_shift);
assert!(d > 0);
next_deleted.pop();
d -= 1;
} else {
break;
}
}
index += d;
}
for v in value.iter() {
match v {
ValueOrHandler::Value(v) => {
self.insert(index, v.clone())?;
update_on_insert(&mut to_delete, index, 1);
index += 1;
index_shift += 1;
}
ValueOrHandler::Handler(h) => {
let old_id = h.id();
let new_h = self.insert_container(
index,
Handler::new_unattached(old_id.container_type()),
)?;
let new_id = new_h.id();
on_container_remap(old_id, new_id);
if let Some(old_index) = to_delete.remove(&old_id) {
if old_index > index {
self.mov(old_index, index)?;
next_deleted.push(Reverse(old_index));
index += 1;
index_shift += 1;
} else {
// we need to sub 1 because old_index < index, and index means the position before the move
// but the param is the position after the move
self.mov(old_index, index - 1)?;
}
deleted.push(old_index);
update_on_delete(&mut to_delete, old_index);
update_on_insert(&mut to_delete, index, 1);
} else {
let new_h = self.insert_container(
index,
Handler::new_unattached(old_id.container_type()),
)?;
let new_id = new_h.id();
on_container_remap(old_id, new_id);
update_on_insert(&mut to_delete, index, 1);
index += 1;
index_shift += 1;
}
}
}
}
}
}
}
// apply the rest of the deletions
// sort deleted indexes from large to small
deleted.sort_by_key(|x| -(*x as i32));
let mut index = 0;
for d in delta.iter() {
match d {
loro_delta::DeltaItem::Retain { len, .. } => {
index += len;
}
loro_delta::DeltaItem::Replace { delete, value, .. } => {
if *delete > 0 {
let mut d = *delete;
while let Some(last) = deleted.last() {
if *last < index + d {
deleted.pop();
d -= 1;
} else {
break;
}
}
index += 1;
self.delete(index, d)?;
}
index += value.len();
}
}
}

View file

@ -3,13 +3,14 @@ use std::collections::VecDeque;
use fractional_index::FractionalIndex;
use fxhash::FxHashMap;
use loro_common::{
ContainerID, ContainerType, Counter, LoroResult, LoroTreeError, LoroValue, PeerID, TreeID,
ContainerID, ContainerType, Counter, IdLp, LoroResult, LoroTreeError, LoroValue, PeerID, TreeID,
};
use smallvec::smallvec;
use crate::{
container::tree::tree_op::TreeOp,
delta::{TreeDiffItem, TreeExternalDiff},
state::{FractionalIndexGenResult, TreeParentId},
state::{FractionalIndexGenResult, NodePosition, TreeParentId},
txn::{EventHint, Transaction},
BasicHandler, HandlerTrait, MapHandler,
};
@ -49,19 +50,6 @@ impl TreeInner {
id
}
fn create_with_target(
&mut self,
parent: Option<TreeID>,
index: usize,
target: TreeID,
) -> TreeID {
self.map.insert(target, MapHandler::new_detached());
self.parent_links.insert(target, parent);
let children = self.children_links.entry(parent).or_default();
children.insert(index, target);
target
}
fn mov(&mut self, target: TreeID, new_parent: Option<TreeID>, index: usize) -> LoroResult<()> {
let old_parent = self
.parent_links
@ -267,7 +255,7 @@ impl HandlerTrait for TreeHandler {
impl std::fmt::Debug for TreeHandler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.inner {
MaybeDetached::Detached(_) => write!(f, "TreeHandler Dettached"),
MaybeDetached::Detached(_) => write!(f, "TreeHandler Detached"),
MaybeDetached::Attached(a) => write!(f, "TreeHandler {}", a.id),
}
}
@ -296,21 +284,15 @@ impl TreeHandler {
}
}
pub fn delete_with_txn(&self, txn: &mut Transaction, target: TreeID) -> LoroResult<()> {
pub(crate) fn delete_with_txn(&self, txn: &mut Transaction, target: TreeID) -> LoroResult<()> {
let inner = self.inner.try_attached_state()?;
txn.apply_local_op(
inner.container_idx,
crate::op::RawOpContent::Tree(TreeOp::Delete { target }),
EventHint::Tree(TreeDiffItem {
EventHint::Tree(smallvec![TreeDiffItem {
target,
action: TreeExternalDiff::Delete {
old_parent: self
.get_node_parent(&target)
.map(TreeParentId::from)
.unwrap_or(TreeParentId::Unexist),
old_index: self.get_index_by_tree_id(&target).unwrap_or(0),
},
}),
action: TreeExternalDiff::Delete,
}]),
&inner.state,
)
}
@ -338,45 +320,141 @@ impl TreeHandler {
}
/// For undo/redo, Specify the TreeID of the created node
pub(crate) fn create_at_with_target(
pub(crate) fn create_at_with_target_for_apply_diff(
&self,
parent: Option<TreeID>,
index: usize,
position: FractionalIndex,
target: TreeID,
) -> LoroResult<()> {
if let Some(p) = parent {
if !self.contains(p) {
return Ok(());
) -> LoroResult<bool> {
let MaybeDetached::Attached(a) = &self.inner else {
unreachable!();
};
if let Some(p) = self.get_node_parent(&target) {
if p == parent {
return Ok(false);
// If parent is deleted, we need to create the node, so this op from move_apply_diff
} else if !p.is_some_and(|p| !self.contains(p)) {
return self.move_at_with_target_for_apply_diff(parent, position, target);
}
}
match &self.inner {
MaybeDetached::Detached(t) => {
let t = &mut t.try_lock().unwrap().value;
t.create_with_target(parent, index, target);
Ok(())
}
MaybeDetached::Attached(a) => a.with_txn(|txn| {
let inner = self.inner.try_attached_state()?;
match self.generate_position_at(&target, parent, index) {
FractionalIndexGenResult::Ok(position) => {
self.create_with_position(inner, txn, target, parent, index, position)?;
}
FractionalIndexGenResult::Rearrange(ids) => {
for (i, (id, position)) in ids.into_iter().enumerate() {
if i == 0 {
self.create_with_position(inner, txn, id, parent, index, position)?;
continue;
}
self.mov_with_position(inner, txn, id, parent, index + i, position)?;
}
}
};
Ok(())
}),
let with_event = !parent.is_some_and(|p| !self.contains(p));
if !with_event {
return Ok(false);
}
// println!(
// "create_at_with_target_for_apply_diff: {:?} {:?}",
// target, parent
// );
let index = self
.get_index_by_fractional_index(
parent,
&NodePosition {
position: position.clone(),
idlp: self.next_idlp(),
},
)
// TODO: parent has deleted
.unwrap_or(0);
let children = a.with_txn(|txn| {
let inner = self.inner.try_attached_state()?;
txn.apply_local_op(
inner.container_idx,
crate::op::RawOpContent::Tree(TreeOp::Create {
target,
parent,
position: position.clone(),
}),
EventHint::Tree(smallvec![TreeDiffItem {
target,
action: TreeExternalDiff::Create {
parent,
index,
position: position.clone(),
},
}]),
&inner.state,
)?;
Ok(self.children(Some(target)).unwrap_or_default())
})?;
for child in children {
let position = self.get_position_by_tree_id(&child).unwrap();
self.create_at_with_target_for_apply_diff(Some(target), position, child)?;
}
Ok(true)
}
pub fn create_with_txn<T: Into<Option<TreeID>>>(
/// For undo/redo, Specify the TreeID of the created node
pub(crate) fn move_at_with_target_for_apply_diff(
&self,
parent: Option<TreeID>,
position: FractionalIndex,
target: TreeID,
) -> LoroResult<bool> {
let MaybeDetached::Attached(a) = &self.inner else {
unreachable!();
};
// the move node does not exist, create it
if !self.contains(target) {
return self.create_at_with_target_for_apply_diff(parent, position, target);
}
if let Some(p) = self.get_node_parent(&target) {
if p == parent {
return Ok(false);
}
}
let index = self
.get_index_by_fractional_index(
parent,
&NodePosition {
position: position.clone(),
idlp: self.next_idlp(),
},
)
.unwrap_or(0);
let with_event = !parent.is_some_and(|p| !self.contains(p));
if !with_event {
return Ok(false);
}
// println!(
// "move_at_with_target_for_apply_diff: {:?} {:?}",
// target, parent
// );
a.with_txn(|txn| {
let inner = self.inner.try_attached_state()?;
txn.apply_local_op(
inner.container_idx,
crate::op::RawOpContent::Tree(TreeOp::Move {
target,
parent,
position: position.clone(),
}),
EventHint::Tree(smallvec![TreeDiffItem {
target,
action: TreeExternalDiff::Move {
parent,
index,
position: position.clone(),
},
}]),
&inner.state,
)
})?;
Ok(true)
}
pub(crate) fn create_with_txn<T: Into<Option<TreeID>>>(
&self,
txn: &mut Transaction,
parent: T,
@ -465,7 +543,7 @@ impl TreeHandler {
}
}
pub fn mov_with_txn<T: Into<Option<TreeID>>>(
pub(crate) fn mov_with_txn<T: Into<Option<TreeID>>>(
&self,
txn: &mut Transaction,
target: TreeID,
@ -513,6 +591,7 @@ impl TreeHandler {
}
}
#[allow(clippy::too_many_arguments)]
fn create_with_position(
&self,
inner: &BasicHandler,
@ -529,19 +608,20 @@ impl TreeHandler {
parent,
position: position.clone(),
}),
EventHint::Tree(TreeDiffItem {
EventHint::Tree(smallvec![TreeDiffItem {
target: tree_id,
action: TreeExternalDiff::Create {
parent,
index,
position,
},
}),
}]),
&inner.state,
)?;
Ok(tree_id)
}
#[allow(clippy::too_many_arguments)]
fn mov_with_position(
&self,
inner: &BasicHandler,
@ -558,19 +638,14 @@ impl TreeHandler {
parent,
position: position.clone(),
}),
EventHint::Tree(TreeDiffItem {
EventHint::Tree(smallvec![TreeDiffItem {
target,
action: TreeExternalDiff::Move {
parent,
index,
position,
old_parent: self
.get_node_parent(&target)
.map(TreeParentId::from)
.unwrap_or(TreeParentId::Unexist),
old_index: self.get_index_by_tree_id(&target).unwrap_or(0),
},
}),
}]),
&inner.state,
)
}
@ -623,8 +698,7 @@ impl TreeHandler {
}
MaybeDetached::Attached(a) => a.with_state(|state| {
let a = state.as_tree_state().unwrap();
a.get_children(&TreeParentId::from(parent))
.map(|x| x.collect())
a.children(&TreeParentId::from(parent))
}),
}
}
@ -761,4 +835,30 @@ impl TreeHandler {
a.delete_position(&TreeParentId::from(parent), target)
})
}
// use for apply diff
pub(crate) fn get_index_by_fractional_index(
&self,
parent: Option<TreeID>,
node_position: &NodePosition,
) -> Option<usize> {
match &self.inner {
MaybeDetached::Detached(_) => {
unreachable!();
}
MaybeDetached::Attached(a) => a.with_state(|state| {
let a = state.as_tree_state().unwrap();
a.get_index_by_position(&TreeParentId::from(parent), node_position)
}),
}
}
pub(crate) fn next_idlp(&self) -> IdLp {
match &self.inner {
MaybeDetached::Detached(_) => {
unreachable!()
}
MaybeDetached::Attached(a) => a.with_txn(|txn| Ok(txn.next_idlp())).unwrap(),
}
}
}

View file

@ -831,6 +831,8 @@ impl LoroDoc {
before_diff,
);
// println!("\nundo_internal: diff: {:?}", diff);
self.checkout_without_emitting(&latest_frontiers)?;
self.detached.store(false, Release);
if was_recording {
@ -927,10 +929,7 @@ impl LoroDoc {
}
let h = self.get_handler(id);
h.apply_diff(diff, &mut |old_id, new_id| {
container_remap.insert(old_id, new_id);
})
.unwrap();
h.apply_diff(diff, container_remap).unwrap();
}
Ok(())
@ -1083,7 +1082,6 @@ impl LoroDoc {
format!("Cannot find the specified version {:?}", frontiers).into_boxed_str(),
));
};
let diff = calc.calc_diff_internal(
&oplog,
before,

View file

@ -100,7 +100,6 @@ impl Observer {
self.inner.lock().unwrap().event_queue.push(doc_diff);
return;
}
let mut inner = self.take_inner();
self.emit_inner(&doc_diff, &mut inner);
self.reset_inner(inner);

View file

@ -8,12 +8,13 @@ use enum_dispatch::enum_dispatch;
use fxhash::{FxHashMap, FxHashSet};
use loro_common::{ContainerID, LoroError, LoroResult};
use loro_delta::DeltaItem;
use tracing::{info, instrument};
use tracing::instrument;
use crate::{
configure::{Configure, DefaultRandom, SecureRandomGenerator},
container::{idx::ContainerIdx, richtext::config::StyleConfigMap, ContainerIdRaw},
cursor::Cursor,
delta::TreeExternalDiff,
diff_calc::DiffCalculator,
encoding::{StateSnapshotDecodeContext, StateSnapshotEncoder},
event::{Diff, EventTriggerKind, Index, InternalContainerDiff, InternalDiff},
@ -39,7 +40,9 @@ pub(crate) use self::movable_list_state::{IndexType, MovableListState};
pub(crate) use list_state::ListState;
pub(crate) use map_state::MapState;
pub(crate) use richtext_state::RichtextState;
pub(crate) use tree_state::{get_meta_value, FractionalIndexGenResult, TreeParentId, TreeState};
pub(crate) use tree_state::{
get_meta_value, FractionalIndexGenResult, NodePosition, TreeParentId, TreeState,
};
use self::unknown_state::UnknownState;
@ -444,7 +447,6 @@ impl DocState {
// We need to ensure diff is processed in order
diffs.sort_by_cached_key(|diff| self.arena.get_depth(diff.idx).unwrap());
let mut to_revive_in_next_layer: FxHashSet<ContainerIdx> = FxHashSet::default();
let mut to_revive_in_this_layer: FxHashSet<ContainerIdx> = FxHashSet::default();
let mut last_depth = 0;
@ -470,9 +472,13 @@ impl DocState {
let external_diff =
state.to_diff(&self.arena, &self.global_txn, &self.weak_state);
trigger_on_new_container(&external_diff, |cid| {
to_revive_in_this_layer.insert(cid);
});
trigger_on_new_container(
&external_diff,
|cid| {
to_revive_in_this_layer.insert(cid);
},
&self.arena,
);
diffs.push(InternalContainerDiff {
idx: new,
@ -493,9 +499,13 @@ impl DocState {
let state = get_or_create!(self, diff.idx);
let extern_diff =
state.to_diff(&self.arena, &self.global_txn, &self.weak_state);
trigger_on_new_container(&extern_diff, |cid| {
to_revive_in_next_layer.insert(cid);
});
trigger_on_new_container(
&extern_diff,
|cid| {
to_revive_in_next_layer.insert(cid);
},
&self.arena,
);
diff.diff = extern_diff.into();
}
}
@ -523,9 +533,13 @@ impl DocState {
&self.weak_state,
)
};
trigger_on_new_container(&external_diff, |cid| {
to_revive_in_next_layer.insert(cid);
});
trigger_on_new_container(
&external_diff,
|cid| {
to_revive_in_next_layer.insert(cid);
},
&self.arena,
);
diff.diff = external_diff.into();
} else {
state.apply_diff(
@ -540,7 +554,9 @@ impl DocState {
}
to_revive_in_this_layer.remove(&idx);
diffs.push(diff);
if !diff.diff.is_empty() {
diffs.push(diff);
}
}
// Revive the last several layers
@ -559,16 +575,22 @@ impl DocState {
}
let external_diff = state.to_diff(&self.arena, &self.global_txn, &self.weak_state);
trigger_on_new_container(&external_diff, |cid| {
to_revive_in_next_layer.insert(cid);
});
trigger_on_new_container(
&external_diff,
|cid| {
to_revive_in_next_layer.insert(cid);
},
&self.arena,
);
diffs.push(InternalContainerDiff {
idx: new,
bring_back: true,
is_container_deleted: false,
diff: external_diff.into(),
});
if !external_diff.is_empty() {
diffs.push(InternalContainerDiff {
idx: new,
bring_back: true,
is_container_deleted: false,
diff: external_diff.into(),
});
}
}
to_revive_in_this_layer = std::mem::take(&mut to_revive_in_next_layer);
@ -1054,7 +1076,7 @@ impl DocState {
// this container may be deleted
let Ok(prop) = id.clone().into_root() else {
let id = format!("{}", &id);
info!(?id, "Missing parent - container is deleted");
tracing::info!(?id, "Missing parent - container is deleted");
return None;
};
ans.push((id, Index::Key(prop.0)));
@ -1309,7 +1331,11 @@ impl DocState {
}
}
fn trigger_on_new_container(state_diff: &Diff, mut listener: impl FnMut(ContainerIdx)) {
fn trigger_on_new_container(
state_diff: &Diff,
mut listener: impl FnMut(ContainerIdx),
arena: &SharedArena,
) {
match state_diff {
Diff::List(list) => {
for delta in list.iter() {
@ -1340,6 +1366,14 @@ fn trigger_on_new_container(state_diff: &Diff, mut listener: impl FnMut(Containe
}
}
}
Diff::Tree(tree) => {
for item in tree.iter() {
if matches!(item.action, TreeExternalDiff::Create { .. }) {
let id = item.target.associated_meta_container();
listener(arena.id_to_idx(&id).unwrap());
}
}
}
_ => {}
};
}

View file

@ -57,7 +57,7 @@ impl From<Option<TreeID>> for TreeParentId {
}
}
#[derive(Clone)]
#[derive(Debug, Clone)]
enum NodeChildren {
Vec(Vec<(NodePosition, TreeID)>),
BTree(btree::ChildTree),
@ -77,6 +77,16 @@ impl NodeChildren {
}
}
fn get_last_insert_index_by_position(
&self,
node_position: &NodePosition,
) -> Result<usize, usize> {
match self {
NodeChildren::Vec(v) => v.binary_search_by_key(&node_position, |x| &x.0),
NodeChildren::BTree(btree) => btree.get_index_by_node_position(node_position),
}
}
fn get_node_position_at(&self, pos: usize) -> Option<&NodePosition> {
match self {
NodeChildren::Vec(v) => v.get(pos).map(|x| &x.0),
@ -322,6 +332,33 @@ mod btree {
Some(ans)
}
pub(super) fn get_index_by_node_position(
&self,
node_position: &NodePosition,
) -> Result<usize, usize> {
let Some(res) = self.tree.query::<KeyQuery>(node_position) else {
return Ok(0);
};
let mut ans = 0;
self.tree
.visit_previous_caches(res.cursor, |prev| match prev {
generic_btree::PreviousCache::NodeCache(c) => {
ans += c.len;
}
generic_btree::PreviousCache::PrevSiblingElem(_) => {
ans += 1;
}
generic_btree::PreviousCache::ThisElemAndOffset { elem: _, offset } => {
ans += offset;
}
});
if res.found {
Ok(ans)
} else {
Err(ans)
}
}
}
#[derive(Clone, Debug)]
@ -525,13 +562,13 @@ pub struct TreeState {
jitter: u8,
}
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)]
struct NodePosition {
position: FractionalIndex,
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct NodePosition {
pub(crate) position: FractionalIndex,
// different nodes created by a peer may have the same position
// when we merge updates that cause cycles.
// for example [::fuzz::test::test_tree::same_peer_have_same_position()]
idlp: IdLp,
pub(crate) idlp: IdLp,
}
impl NodePosition {
@ -584,22 +621,10 @@ impl TreeState {
self.delete_position(&old_parent, target);
}
if !parent.is_deleted() {
let entry = self.children.entry(parent).or_default();
let node_position = NodePosition::new(position.clone().unwrap(), id.idlp());
debug_assert!(!entry.has_child(&node_position));
entry.insert_child(node_position, target);
} else {
// clean the cache recursively, otherwise the index of event will be calculated incorrectly
let mut q = vec![target];
while let Some(id) = q.pop() {
let parent = TreeParentId::from(Some(id));
if let Some(children) = self.children.get(&parent) {
q.extend(children.iter().map(|x| x.1));
}
self.children.remove(&parent);
}
}
let entry = self.children.entry(parent).or_default();
let node_position = NodePosition::new(position.clone().unwrap_or_default(), id.idlp());
debug_assert!(!entry.has_child(&node_position));
entry.insert_child(node_position, target);
self.trees.insert(
target,
@ -725,11 +750,10 @@ impl TreeState {
self.children.get(parent).map(|x| x.len())
}
pub fn children(&self, parent: &TreeParentId) -> Vec<TreeID> {
pub fn children(&self, parent: &TreeParentId) -> Option<Vec<TreeID>> {
self.children
.get(parent)
.map(|x| x.iter().map(|x| *x.1).collect())
.unwrap_or_default()
}
/// Determine whether the target is the child of the node
@ -782,6 +806,19 @@ impl TreeState {
.flatten()
}
pub(crate) fn get_index_by_position(
&self,
parent: &TreeParentId,
node_position: &NodePosition,
) -> Option<usize> {
self.children.get(parent).map(|c| {
match c.get_last_insert_index_by_position(node_position) {
Ok(i) => i,
Err(i) => i,
}
})
}
pub(crate) fn get_id_by_index(&self, parent: &TreeParentId, index: usize) -> Option<TreeID> {
(!parent.is_deleted())
.then(|| self.children.get(parent).and_then(|x| x.get_id_at(index)))
@ -838,8 +875,6 @@ impl ContainerState for TreeState {
});
}
TreeInternalDiff::Move { parent, position } => {
let old_parent = self.trees.get(&target).unwrap().parent;
let old_index = self.get_index_by_tree_id(&target).unwrap();
self.mov(target, *parent, last_move_op, Some(position.clone()), false)
.unwrap();
let index = self.get_index_by_tree_id(&target).unwrap();
@ -849,22 +884,15 @@ impl ContainerState for TreeState {
parent: parent.into_node().ok(),
index,
position: position.clone(),
old_parent,
old_index,
},
});
}
TreeInternalDiff::Delete { parent, position } => {
let old_parent = self.trees.get(&target).unwrap().parent;
let old_index = self.get_index_by_tree_id(&target).unwrap();
self.mov(target, *parent, last_move_op, position.clone(), false)
.unwrap();
ans.push(TreeDiffItem {
target,
action: TreeExternalDiff::Delete {
old_parent,
old_index,
},
action: TreeExternalDiff::Delete,
});
}
TreeInternalDiff::MoveInDelete { parent, position } => {
@ -872,15 +900,13 @@ impl ContainerState for TreeState {
.unwrap();
}
TreeInternalDiff::UnCreate => {
let old_parent = self.trees.get(&target).unwrap().parent;
let old_index = self.get_index_by_tree_id(&target).unwrap();
ans.push(TreeDiffItem {
target,
action: TreeExternalDiff::Delete {
old_parent,
old_index,
},
});
// maybe the node created and moved to the parent deleted
if !self.is_node_deleted(&target) {
ans.push(TreeDiffItem {
target,
action: TreeExternalDiff::Delete,
});
}
// delete it from state
let parent = self.trees.remove(&target);
if let Some(parent) = parent {

View file

@ -123,7 +123,8 @@ pub(super) enum EventHint {
key: InternalString,
value: Option<LoroValue>,
},
Tree(TreeDiffItem),
// use vec because we could bring back some node that has children
Tree(SmallVec<[TreeDiffItem; 1]>),
MarkEnd,
#[cfg(feature = "counter")]
Counter(f64),
@ -393,6 +394,7 @@ impl Transaction {
let op = self.arena.convert_raw_op(&raw_op);
state.apply_local_op(&raw_op, &op)?;
drop(state);
debug_assert_eq!(
event.rle_len(),
op.atom_len(),
@ -400,6 +402,7 @@ impl Transaction {
&event,
&op
);
match self.event_hints.last_mut() {
Some(last) if last.can_merge(&event) => {
last.merge_right(&event);
@ -490,6 +493,13 @@ impl Transaction {
}
}
pub fn next_idlp(&self) -> IdLp {
IdLp {
peer: self.peer,
lamport: self.next_lamport,
}
}
pub fn is_empty(&self) -> bool {
self.local_ops.is_empty()
}
@ -657,7 +667,7 @@ fn change_to_diff(
}),
EventHint::Tree(tree_diff) => {
let mut diff = TreeDiff::default();
diff.push(tree_diff);
diff.diff.extend(tree_diff.into_iter());
ans.push(TxnContainerDiff {
idx: op.container,
diff: Diff::Tree(diff),
@ -716,6 +726,5 @@ fn change_to_diff(
.map(|x| x.content_len() as Lamport)
.sum::<Lamport>();
}
ans
}

View file

@ -40,9 +40,11 @@ impl DiffBatch {
return;
}
for (idx, diff) in self.0.iter_mut() {
if let Some(b_diff) = other.0.get(idx) {
diff.compose_ref(b_diff);
for (idx, diff) in other.0.iter() {
if let Some(this_diff) = self.0.get_mut(idx) {
this_diff.compose_ref(diff);
} else {
self.0.insert(idx.clone(), diff.clone());
}
}
}
@ -146,7 +148,7 @@ pub type OnPush = Box<dyn Fn(UndoOrRedo, CounterSpan) -> UndoItemMeta + Send + S
pub type OnPop = Box<dyn Fn(UndoOrRedo, CounterSpan, UndoItemMeta) + Send + Sync>;
struct UndoManagerInner {
latest_counter: Counter,
latest_counter: Option<Counter>,
undo_stack: Stack,
redo_stack: Stack,
processing_undo: bool,
@ -180,7 +182,7 @@ struct Stack {
size: usize,
}
#[derive(Debug)]
#[derive(Debug, Clone)]
struct StackItem {
span: CounterSpan,
meta: UndoItemMeta,
@ -210,7 +212,7 @@ impl UndoItemMeta {
}
}
/// It's assumed that the cursor is just acqured before the ops that
/// It's assumed that the cursor is just acquired before the ops that
/// need to be undo/redo.
///
/// We need to rely on the validity of the original pos value
@ -271,19 +273,14 @@ impl Stack {
pub fn push_with_merge(&mut self, span: CounterSpan, meta: UndoItemMeta, can_merge: bool) {
let last = self.stack.back_mut().unwrap();
let mut last_remote_diff = last.1.try_lock().unwrap();
let last_remote_diff = last.1.try_lock().unwrap();
if !last_remote_diff.0.is_empty() {
// If the remote diff is not empty, we cannot merge
if last.0.is_empty() {
last.0.push_back(StackItem { span, meta });
last_remote_diff.clear();
} else {
drop(last_remote_diff);
let mut v = VecDeque::new();
v.push_back(StackItem { span, meta });
self.stack
.push_back((v, Arc::new(Mutex::new(DiffBatch::default()))));
}
drop(last_remote_diff);
let mut v = VecDeque::new();
v.push_back(StackItem { span, meta });
self.stack
.push_back((v, Arc::new(Mutex::new(DiffBatch::default()))));
self.size += 1;
} else {
@ -322,7 +319,6 @@ impl Stack {
if self.is_empty() {
return;
}
let remote_diff = &mut self.stack.back_mut().unwrap().1;
remote_diff.try_lock().unwrap().transform(diff, false);
}
@ -365,7 +361,7 @@ impl Default for Stack {
impl UndoManagerInner {
fn new(last_counter: Counter) -> Self {
UndoManagerInner {
latest_counter: last_counter,
latest_counter: Some(last_counter),
undo_stack: Default::default(),
redo_stack: Default::default(),
processing_undo: false,
@ -380,13 +376,18 @@ impl UndoManagerInner {
}
fn record_checkpoint(&mut self, latest_counter: Counter) {
if latest_counter == self.latest_counter {
if Some(latest_counter) == self.latest_counter {
return;
}
assert!(self.latest_counter < latest_counter);
if self.latest_counter.is_none() {
self.latest_counter = Some(latest_counter);
return;
}
assert!(self.latest_counter.unwrap() < latest_counter);
let now = get_sys_timestamp();
let span = CounterSpan::new(self.latest_counter, latest_counter);
let span = CounterSpan::new(self.latest_counter.unwrap(), latest_counter);
let meta = self
.on_push
.as_ref()
@ -400,7 +401,7 @@ impl UndoManagerInner {
self.undo_stack.push(span, meta);
}
self.latest_counter = latest_counter;
self.latest_counter = Some(latest_counter);
self.redo_stack.clear();
while self.undo_stack.len() > self.max_stack_size {
self.undo_stack.pop_front();
@ -445,7 +446,7 @@ impl UndoManager {
// a remote event.
inner.undo_stack.compose_remote_event(event.events);
inner.redo_stack.compose_remote_event(event.events);
inner.latest_counter = id.counter + 1;
inner.latest_counter = Some(id.counter + 1);
} else {
inner.record_checkpoint(id.counter + 1);
}
@ -456,7 +457,12 @@ impl UndoManager {
inner.undo_stack.compose_remote_event(event.events);
inner.redo_stack.compose_remote_event(event.events);
}
EventTriggerKind::Checkout => {}
EventTriggerKind::Checkout => {
let mut inner = inner_clone.try_lock().unwrap();
inner.undo_stack.clear();
inner.redo_stack.clear();
inner.latest_counter = None;
}
}));
UndoManager {
@ -648,7 +654,7 @@ impl UndoManager {
}
get_opposite(&mut inner).push(CounterSpan::new(end_counter, new_counter), meta);
inner.latest_counter = new_counter;
inner.latest_counter = Some(new_counter);
executed = true;
break;
} else {
@ -749,7 +755,6 @@ pub(crate) fn undo(
// ------------------------------------------------------------------------------
// 1.b Transform and apply Ci-1 based on Ai, call it A'i
// ------------------------------------------------------------------------------
last_ci.transform(&event_a_i, true);
event_a_i.compose(&last_ci);
@ -760,13 +765,12 @@ pub(crate) fn undo(
if i == spans.len() - 1 {
on_last_event_a(&event_a_prime);
}
// --------------------------------------------------
// 3. Transform event A'_i based on B_i, call it C_i
// --------------------------------------------------
event_a_prime.transform(event_b_i, true);
let c_i = event_a_prime;
let c_i = event_a_prime;
last_ci = Some(c_i);
});
}

View file

@ -720,26 +720,18 @@ fn map_concurrent_checkout() {
#[test]
fn tree_checkout() {
let doc_a = LoroDoc::new();
let doc_a = LoroDoc::new_auto_commit();
doc_a.subscribe_root(Arc::new(|_e| {}));
doc_a.set_peer_id(1).unwrap();
let tree = doc_a.get_tree("root");
let id1 = doc_a
.with_txn(|txn| tree.create_with_txn(txn, None, 0))
.unwrap();
let id2 = doc_a
.with_txn(|txn| tree.create_with_txn(txn, id1, 0))
.unwrap();
let id1 = tree.create(None).unwrap();
let id2 = tree.create(id1).unwrap();
let v1_state = tree.get_deep_value();
let v1 = doc_a.oplog_frontiers();
let _id3 = doc_a
.with_txn(|txn| tree.create_with_txn(txn, id2, 0))
.unwrap();
let _id3 = tree.create(id2).unwrap();
let v2_state = tree.get_deep_value();
let v2 = doc_a.oplog_frontiers();
doc_a
.with_txn(|txn| tree.delete_with_txn(txn, id2))
.unwrap();
tree.delete(id2).unwrap();
let v3_state = tree.get_deep_value();
let v3 = doc_a.oplog_frontiers();
doc_a.checkout(&v1).unwrap();
@ -765,12 +757,7 @@ fn tree_checkout() {
);
doc_a.attach();
doc_a
.with_txn(|txn| {
tree.create_with_txn(txn, None, 0)
//tree.insert_meta(txn, id1, "a", 1.into())
})
.unwrap();
tree.create(None).unwrap();
}
#[test]

View file

@ -10,7 +10,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
js-sys = "0.3.60"
loro-internal = { path = "../loro-internal", features = ["wasm"] }
loro-internal = { path = "../loro-internal", features = ["wasm", "counter"] }
wasm-bindgen = "=0.2.92"
serde-wasm-bindgen = { version = "^0.6.5" }
wasm-bindgen-derive = "0.2.1"
@ -24,4 +24,3 @@ serde_json = "1"
[features]
default = ["console_error_panic_hook"]
counter = ["loro-internal/counter"]

View file

@ -59,7 +59,7 @@ async function build() {
async function cargoBuild() {
const cmd =
`cargo build --features counter --target wasm32-unknown-unknown --profile ${profile}`;
`cargo build --target wasm32-unknown-unknown --profile ${profile}`;
console.log(cmd);
const status = await Deno.run({
cmd: cmd.split(" "),

View file

@ -9,15 +9,12 @@ use loro_internal::{ListDiffItem, LoroDoc, LoroValue};
use wasm_bindgen::JsValue;
use crate::{
frontiers_to_ids, Container, Cursor, JsContainer, JsImportBlobMetadata, LoroList, LoroMap,
LoroMovableList, LoroText, LoroTree,
frontiers_to_ids, Container, Cursor, JsContainer, JsImportBlobMetadata, LoroCounter, LoroList,
LoroMap, LoroMovableList, LoroText, LoroTree,
};
use wasm_bindgen::__rt::IntoJsResult;
use wasm_bindgen::convert::RefFromWasmAbi;
#[cfg(feature = "counter")]
use crate::LoroCounter;
/// Convert a `JsValue` to `T` by constructor's name.
///
/// more details can be found in https://github.com/rustwasm/wasm-bindgen/issues/2231#issuecomment-656293288
@ -137,7 +134,6 @@ pub(crate) fn resolved_diff_to_js(value: &Diff, doc: &Arc<LoroDoc>) -> JsValue {
.unwrap();
}
#[cfg(feature = "counter")]
Diff::Counter(v) => {
js_sys::Reflect::set(
&obj,
@ -345,7 +341,6 @@ pub(crate) fn handler_to_js_value(handler: Handler, doc: Option<Arc<LoroDoc>>) -
Handler::List(l) => LoroList { handler: l, doc }.into(),
Handler::Tree(t) => LoroTree { handler: t, doc }.into(),
Handler::MovableList(m) => LoroMovableList { handler: m, doc }.into(),
#[cfg(feature = "counter")]
Handler::Counter(c) => LoroCounter { handler: c, doc }.into(),
Handler::Unknown(_) => unreachable!(),
}

View file

@ -29,12 +29,9 @@ use std::{cell::RefCell, cmp::Ordering, rc::Rc, sync::Arc};
use wasm_bindgen::{__rt::IntoJsResult, prelude::*, throw_val};
use wasm_bindgen_derive::TryFromJsValue;
#[cfg(feature = "counter")]
mod counter;
#[cfg(feature = "counter")]
pub use counter::LoroCounter;
#[cfg(feature = "counter")]
use loro_internal::handler::counter::CounterHandler;
mod awareness;
mod log;
@ -316,7 +313,7 @@ impl Loro {
/// If enabled, the Unix timestamp will be recorded for each change automatically.
///
/// You can also set each timestamp manually when you commit a change.
/// The timstamp manually set will override the automatic one.
/// The timestamp manually set will override the automatic one.
///
/// NOTE: Timestamps are forced to be in ascending order.
/// If you commit a new change with a timestamp that is less than the existing one,
@ -696,7 +693,6 @@ impl Loro {
}
/// Get a LoroCounter by container id
#[cfg(feature = "counter")]
#[wasm_bindgen(js_name = "getCounter")]
pub fn get_counter(&self, cid: &JsIntoContainerID) -> JsResult<LoroCounter> {
let counter = self
@ -789,7 +785,6 @@ impl Loro {
}
.into()
}
#[cfg(feature = "counter")]
ContainerType::Counter => {
let counter = self.0.get_counter(container_id);
LoroCounter {
@ -1713,7 +1708,7 @@ impl LoroText {
}
}
/// Whether the container is attached to a docuemnt.
/// Whether the container is attached to a document.
///
/// If it's detached, the operations on the container will not be persisted.
#[wasm_bindgen(js_name = "isAttached")]
@ -2070,7 +2065,7 @@ impl LoroMap {
}
}
/// Whether the container is attached to a docuemnt.
/// Whether the container is attached to a document.
///
/// If it's detached, the operations on the container will not be persisted.
#[wasm_bindgen(js_name = "isAttached")]
@ -2358,7 +2353,7 @@ impl LoroList {
}
}
/// Whether the container is attached to a docuemnt.
/// Whether the container is attached to a document.
///
/// If it's detached, the operations on the container will not be persisted.
#[wasm_bindgen(js_name = "isAttached")]
@ -2685,7 +2680,7 @@ impl LoroMovableList {
}
}
/// Whether the container is attached to a docuemnt.
/// Whether the container is attached to a document.
///
/// If it's detached, the operations on the container will not be persisted.
#[wasm_bindgen(js_name = "isAttached")]
@ -2999,13 +2994,15 @@ impl LoroTreeNode {
/// The objects returned are new js objects each time because they need to cross
/// the WASM boundary.
#[wasm_bindgen(skip_typescript)]
pub fn children(&self) -> Array {
let children = self.tree.children(Some(self.id)).unwrap_or_default();
pub fn children(&self) -> JsValue {
let Some(children) = self.tree.children(Some(self.id)) else {
return JsValue::undefined();
};
let children = children.into_iter().map(|c| {
let node = LoroTreeNode::from_tree(c, self.tree.clone(), self.doc.clone());
JsValue::from(node)
});
Array::from_iter(children)
Array::from_iter(children).into()
}
}
@ -3316,7 +3313,7 @@ impl LoroTree {
}
}
/// Whether the container is attached to a docuemnt.
/// Whether the container is attached to a document.
///
/// If it's detached, the operations on the container will not be persisted.
#[wasm_bindgen(js_name = "isAttached")]
@ -3395,7 +3392,7 @@ impl Cursor {
/// Get the ID that represents the position.
///
/// It can be undefined if it's not binded into a specific ID.
/// It can be undefined if it's not bind into a specific ID.
pub fn pos(&self) -> Option<JsID> {
match self.pos.id {
Some(id) => {

View file

@ -1377,6 +1377,8 @@ impl LoroTree {
}
/// Return all children of the target node.
///
/// If the parent node does not exist, return `None`.
pub fn children(&self, parent: Option<TreeID>) -> Option<Vec<TreeID>> {
self.handler.children(parent)
}

View file

@ -1209,7 +1209,8 @@ fn undo_tree_concurrent_delete2() -> LoroResult<()> {
.get("id")
.unwrap()
.to_json_value(),
json!("1@1")
// create a new node
json!("1@2")
);
Ok(())
}

View file

@ -638,7 +638,7 @@ declare module "loro-wasm" {
* The objects returned are new js objects each time because they need to cross
* the WASM boundary.
*/
children(): Array<LoroTreeNode<T>>;
children(): Array<LoroTreeNode<T>> | undefined;
}
interface AwarenessWasm<T extends Value = Value> {

View file

@ -34,7 +34,7 @@ describe("loro tree", () => {
assertEquals(child2.parent()!.id, root.id);
tree.move(child2.id, child.id);
assertEquals(child2.parent()!.id, child.id);
assertEquals(child.children()[0].id, child2.id);
assertEquals(child.children()![0].id, child2.id);
expect(()=>tree.move(child2.id, child.id, 1)).toThrowError();
});
@ -70,9 +70,9 @@ describe("loro tree", () => {
const root = tree.createNode();
const child = tree.createNode(root.id);
const child2 = tree.createNode(root.id);
assertEquals(root.children().length, 2);
assertEquals(root.children()[0].id, child.id);
assertEquals(root.children()[1].id, child2.id);
assertEquals(root.children()!.length, 2);
assertEquals(root.children()![0].id, child.id);
assertEquals(root.children()![1].id, child2.id);
});
it("toArray", ()=>{
@ -141,7 +141,7 @@ describe("loro tree node", ()=>{
assertEquals(child2.parent()!.id, root.id);
child2.move(child);
assertEquals(child2.parent()!.id, child.id);
assertEquals(child.children()[0].id, child2.id);
assertEquals(child.children()![0].id, child2.id);
expect(()=>child2.move(child, 1)).toThrowError();
});