mirror of
https://github.com/loro-dev/loro.git
synced 2025-02-05 20:17:13 +00:00
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:
parent
fc46f429f1
commit
9047065843
33 changed files with 7453 additions and 491 deletions
29
Cargo.lock
generated
29
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"
|
||||
|
|
30
crates/fuzz/fuzz/Cargo.lock
generated
30
crates/fuzz/fuzz/Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
@ -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}")]
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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(" "),
|
||||
|
|
|
@ -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!(),
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue