From 07671ea9fd073d6d32f4969667ecd5a58e184f90 Mon Sep 17 00:00:00 2001 From: Leon Zhao Date: Mon, 9 Sep 2024 16:16:02 +0800 Subject: [PATCH] feat: add old parent and old index in tree diff (#452) * feat: add old parent in tree diff * chore: enable ci * feat: add old_index to tree diff * fix: new fractional index config * fix: cargo fix * fix: add FractionalIndexNotEnabled error * fix: move config to tree state * fix: error string --------- Co-authored-by: Zixuan Chen --- .github/workflows/rust.yml | 2 +- crates/fuzz/fuzz/Cargo.lock | 1 + crates/fuzz/src/actor.rs | 6 +- crates/fuzz/src/container/tree.rs | 15 +- crates/loro-common/src/error.rs | 2 + crates/loro-common/src/lib.rs | 2 +- crates/loro-internal/benches/tree.rs | 31 ++- crates/loro-internal/examples/tree.rs | 16 +- crates/loro-internal/src/configure.rs | 27 +-- crates/loro-internal/src/delta/tree.rs | 11 +- crates/loro-internal/src/diff_calc/tree.rs | 2 +- crates/loro-internal/src/handler.rs | 17 +- crates/loro-internal/src/handler/tree.rs | 220 +++++++++++------- crates/loro-internal/src/lib.rs | 1 + crates/loro-internal/src/loro.rs | 18 +- crates/loro-internal/src/oplog.rs | 2 +- .../loro-internal/src/oplog/change_store.rs | 10 +- crates/loro-internal/src/state.rs | 31 +-- .../src/state/container_store.rs | 14 +- .../src/state/movable_list_state.rs | 3 +- crates/loro-internal/src/state/tree_state.rs | 205 ++++++++++------ crates/loro-internal/src/utils/kv_wrapper.rs | 8 +- crates/loro-internal/src/value.rs | 40 +++- crates/loro-internal/tests/test.rs | 15 +- crates/loro-internal/tests/tree.rs | 25 ++ crates/loro-wasm/src/lib.rs | 63 +++-- crates/loro/src/change_meta.rs | 2 +- crates/loro/src/lib.rs | 99 ++++---- loro-js/src/index.ts | 8 +- loro-js/tests/tree.test.ts | 24 +- 30 files changed, 549 insertions(+), 371 deletions(-) create mode 100644 crates/loro-internal/tests/tree.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 13d71fc7..3ffed61b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -4,7 +4,7 @@ on: push: branches: ["main"] pull_request: - branches: ["main"] + branches: ["main", "dev"] types: [opened, synchronize, reopened, ready_for_review] env: diff --git a/crates/fuzz/fuzz/Cargo.lock b/crates/fuzz/fuzz/Cargo.lock index eccb7525..ef6e54b2 100644 --- a/crates/fuzz/fuzz/Cargo.lock +++ b/crates/fuzz/fuzz/Cargo.lock @@ -780,6 +780,7 @@ name = "loro_fractional_index" version = "0.16.2" dependencies = [ "imbl", + "once_cell", "rand", "serde", "smallvec", diff --git a/crates/fuzz/src/actor.rs b/crates/fuzz/src/actor.rs index b9c32759..7aaf106c 100644 --- a/crates/fuzz/src/actor.rs +++ b/crates/fuzz/src/actor.rs @@ -24,8 +24,6 @@ use super::{ container::MapActor, }; -const DEFAULT_WITH_FRACTIONAL_INDEX: bool = false; - #[derive(Debug)] pub struct Undo { pub undo: UndoManager, @@ -47,7 +45,6 @@ impl Actor { pub fn new(id: PeerID) -> Self { let loro = LoroDoc::new(); loro.set_peer_id(id).unwrap(); - loro.set_with_fractional_index(DEFAULT_WITH_FRACTIONAL_INDEX); let undo = UndoManager::new(&loro); let tracker = Arc::new(Mutex::new(ContainerTracker::Map(MapTracker::empty( ContainerID::new_root("sys:root", ContainerType::Map), @@ -122,6 +119,9 @@ impl Actor { } if let Some(idx) = idx { + if let Container::Tree(tree) = &idx { + tree.set_enable_fractional_index(0); + } self.add_new_container(idx); } } diff --git a/crates/fuzz/src/container/tree.rs b/crates/fuzz/src/container/tree.rs index 5d9b39c5..83da2058 100644 --- a/crates/fuzz/src/container/tree.rs +++ b/crates/fuzz/src/container/tree.rs @@ -10,7 +10,6 @@ use loro::{ event::Diff, Container, ContainerID, ContainerType, LoroDoc, LoroError, LoroTree, LoroValue, TreeExternalDiff, TreeID, }; -use tracing::{debug, trace}; use crate::{ actions::{Actionable, FromGenericAction, GenericAction}, @@ -117,6 +116,7 @@ impl TreeActor { ); let root = loro.get_tree("tree"); + root.set_enable_fractional_index(0); Self { loro, containers: vec![root], @@ -184,7 +184,7 @@ impl Actionable for TreeAction { } *parent = (nodes[parent_idx].peer, nodes[parent_idx].counter); *index %= tree - .children_num(Some(TreeID::new(parent.0, parent.1))) + .children_num(TreeID::new(parent.0, parent.1)) .unwrap_or(0) + 1; } @@ -426,7 +426,7 @@ impl ApplyDiff for TreeTracker { index, position, } => { - self.create_node(target, parent, position.to_string(), index); + self.create_node(target, &parent.tree_id(), position.to_string(), index); } TreeExternalDiff::Delete { .. } => { let node = self.find_node_by_id(target).unwrap(); @@ -442,9 +442,10 @@ impl ApplyDiff for TreeTracker { parent, index, position, + .. } => { let Some(node) = self.find_node_by_id(target) else { - self.create_node(target, parent, position.to_string(), index); + self.create_node(target, &parent.tree_id(), position.to_string(), index); continue; }; @@ -456,10 +457,10 @@ impl ApplyDiff for TreeTracker { let index = self.tree.iter().position(|n| n.id == target).unwrap(); self.tree.remove(index) }; - node.parent = *parent; + node.parent = parent.tree_id(); node.position = position.to_string(); - if let Some(parent) = parent { - let parent = self.find_node_by_id_mut(*parent).unwrap(); + if let Some(parent) = parent.tree_id() { + let parent = self.find_node_by_id_mut(parent).unwrap(); parent.children.insert(*index, node); } else { if self.find_node_by_id_mut(target).is_some() { diff --git a/crates/loro-common/src/error.rs b/crates/loro-common/src/error.rs index 5fb95463..da727337 100644 --- a/crates/loro-common/src/error.rs +++ b/crates/loro-common/src/error.rs @@ -88,6 +88,8 @@ pub enum LoroTreeError { TreeNodeNotExist(TreeID), #[error("The index({index}) should be <= the length of children ({len})")] IndexOutOfBound { len: usize, index: usize }, + #[error("Fractional index is not enabled, you should enable it first by `LoroTree::set_enable_fractional_index`")] + FractionalIndexNotEnabled, } #[cfg(feature = "wasm")] diff --git a/crates/loro-common/src/lib.rs b/crates/loro-common/src/lib.rs index 891657cf..3b1bbbfd 100644 --- a/crates/loro-common/src/lib.rs +++ b/crates/loro-common/src/lib.rs @@ -1,4 +1,4 @@ -use std::{fmt::Display, io::Write, str::Bytes, sync::Arc}; +use std::{fmt::Display, io::Write, sync::Arc}; use arbitrary::Arbitrary; use enum_as_inner::EnumAsInner; diff --git a/crates/loro-internal/benches/tree.rs b/crates/loro-internal/benches/tree.rs index a0e94f35..94581a04 100644 --- a/crates/loro-internal/benches/tree.rs +++ b/crates/loro-internal/benches/tree.rs @@ -3,7 +3,7 @@ use criterion::{criterion_group, criterion_main, Criterion}; mod tree { use super::*; use criterion::{AxisScale, BenchmarkId, PlotConfiguration}; - use loro_internal::LoroDoc; + use loro_internal::{LoroDoc, TreeParentId}; use rand::{rngs::StdRng, Rng}; pub fn tree_move(c: &mut Criterion) { @@ -22,7 +22,7 @@ mod tree { let loro = LoroDoc::new_auto_commit(); let tree = loro.get_tree("tree"); for idx in 0..*i { - tree.create_at(None, idx as usize).unwrap(); + tree.create_at(TreeParentId::Root, idx as usize).unwrap(); } }) }, @@ -39,15 +39,16 @@ mod tree { let mut ids = vec![]; for _ in 0..SIZE { let pos = rng.gen::() % (ids.len() + 1); - ids.push(tree.create_at(None, pos).unwrap()); + ids.push(tree.create_at(TreeParentId::Root, pos).unwrap()); } b.iter(|| { for _ in 0..*i { - tree.create_at(None, 0).unwrap(); + tree.create_at(TreeParentId::Root, 0).unwrap(); let i = rng.gen::() % SIZE; let j = rng.gen::() % SIZE; - tree.mov(ids[i], ids[j]).unwrap_or_default(); + tree.mov(ids[i], TreeParentId::Node(ids[j])) + .unwrap_or_default(); } }) }, @@ -62,14 +63,14 @@ mod tree { let mut versions = vec![]; let size = 1000; for _ in 0..size { - ids.push(tree.create(None).unwrap()) + ids.push(tree.create(TreeParentId::Root).unwrap()) } let mut rng: StdRng = rand::SeedableRng::seed_from_u64(0); let mut n = 1000; while n > 0 { let i = rng.gen::() % size; let j = rng.gen::() % size; - if tree.mov(ids[i], ids[j]).is_ok() { + if tree.mov(ids[i], TreeParentId::Node(ids[j])).is_ok() { versions.push(loro.oplog_frontiers()); n -= 1; }; @@ -90,11 +91,13 @@ mod tree { let tree = loro.get_tree("tree"); let mut ids = vec![]; let mut versions = vec![]; - let id1 = tree.create(None).unwrap(); + let id1 = tree.create(TreeParentId::Root).unwrap(); ids.push(id1); versions.push(loro.oplog_frontiers()); for _ in 1..depth { - let id = tree.create(*ids.last().unwrap()).unwrap(); + let id = tree + .create(TreeParentId::Node(*ids.last().unwrap())) + .unwrap(); ids.push(id); versions.push(loro.oplog_frontiers()); } @@ -118,7 +121,7 @@ mod tree { let mut ids = vec![]; let size = 1000; for _ in 0..size { - ids.push(tree_a.create(None).unwrap()) + ids.push(tree_a.create(TreeParentId::Root).unwrap()) } doc_b.import(&doc_a.export_snapshot()).unwrap(); let mut rng: StdRng = rand::SeedableRng::seed_from_u64(0); @@ -128,10 +131,14 @@ mod tree { let i = rng.gen::() % size; let j = rng.gen::() % size; if t % 2 == 0 { - tree_a.mov(ids[i], ids[j]).unwrap_or_default(); + tree_a + .mov(ids[i], TreeParentId::Node(ids[j])) + .unwrap_or_default(); doc_b.import(&doc_a.export_from(&doc_b.oplog_vv())).unwrap(); } else { - tree_b.mov(ids[i], ids[j]).unwrap_or_default(); + tree_b + .mov(ids[i], TreeParentId::Node(ids[j])) + .unwrap_or_default(); doc_a.import(&doc_b.export_from(&doc_a.oplog_vv())).unwrap(); } } diff --git a/crates/loro-internal/examples/tree.rs b/crates/loro-internal/examples/tree.rs index 309ef7d3..e89b826a 100644 --- a/crates/loro-internal/examples/tree.rs +++ b/crates/loro-internal/examples/tree.rs @@ -1,6 +1,6 @@ use std::time::Instant; -use loro_internal::LoroDoc; +use loro_internal::{LoroDoc, TreeParentId}; use rand::{rngs::StdRng, Rng}; #[allow(unused)] @@ -10,11 +10,13 @@ fn checkout() { let tree = loro.get_tree("tree"); let mut ids = vec![]; let mut versions = vec![]; - let id1 = tree.create_at(None, 0).unwrap(); + let id1 = tree.create_at(TreeParentId::Root, 0).unwrap(); ids.push(id1); versions.push(loro.oplog_frontiers()); for _ in 1..depth { - let id = tree.create_at(*ids.last().unwrap(), 0).unwrap(); + let id = tree + .create_at(TreeParentId::Node(*ids.last().unwrap()), 0) + .unwrap(); ids.push(id); versions.push(loro.oplog_frontiers()); } @@ -34,7 +36,7 @@ fn mov() { let mut ids = vec![]; let size = 10000; for _ in 0..size { - ids.push(tree.create_at(None, 0).unwrap()) + ids.push(tree.create_at(TreeParentId::Root, 0).unwrap()) } let mut rng: StdRng = rand::SeedableRng::seed_from_u64(0); let n = 100000; @@ -42,8 +44,8 @@ fn mov() { for _ in 0..n { let i = rng.gen::() % size; let j = rng.gen::() % size; - let children_num = tree.children_num(Some(ids[j])).unwrap_or(0); - tree.move_to(ids[i], ids[j], children_num) + let children_num = tree.children_num(&TreeParentId::Node(ids[j])).unwrap_or(0); + tree.move_to(ids[i], TreeParentId::Node(ids[j]), children_num) .unwrap_or_default(); } println!("encode snapshot size {:?}", loro.export_snapshot().len()); @@ -59,7 +61,7 @@ fn create() { let loro = LoroDoc::default(); let tree = loro.get_tree("tree"); for _ in 0..size { - tree.create_at(None, 0).unwrap(); + tree.create_at(TreeParentId::Root, 0).unwrap(); } println!("encode snapshot size {:?}\n", loro.export_snapshot().len()); println!( diff --git a/crates/loro-internal/src/configure.rs b/crates/loro-internal/src/configure.rs index 69364823..e46f102b 100644 --- a/crates/loro-internal/src/configure.rs +++ b/crates/loro-internal/src/configure.rs @@ -5,11 +5,6 @@ pub struct Configure { pub(crate) text_style_config: Arc>, record_timestamp: Arc, pub(crate) merge_interval: Arc, - /// Whether the tree has fractional index. `false` by default. If false, the fractional index is always [`FractionalIndex::default`] and - /// `tree_position_jitter` is not used. - pub(crate) tree_with_fractional_index: Arc, - /// do not use `jitter` by default - pub(crate) tree_position_jitter: Arc, } impl Default for Configure { @@ -18,8 +13,6 @@ impl Default for Configure { text_style_config: Arc::new(RwLock::new(StyleConfigMap::default_rich_text_config())), record_timestamp: Arc::new(AtomicBool::new(false)), merge_interval: Arc::new(AtomicI64::new(1000 * 1000)), - tree_position_jitter: Arc::new(AtomicU8::new(0)), - tree_with_fractional_index: Arc::new(AtomicBool::new(false)), } } } @@ -38,14 +31,6 @@ impl Configure { self.merge_interval .load(std::sync::atomic::Ordering::Relaxed), )), - tree_position_jitter: Arc::new(AtomicU8::new( - self.tree_position_jitter - .load(std::sync::atomic::Ordering::Relaxed), - )), - tree_with_fractional_index: Arc::new(AtomicBool::new( - self.tree_with_fractional_index - .load(std::sync::atomic::Ordering::Relaxed), - )), } } @@ -63,16 +48,6 @@ impl Configure { .store(record, std::sync::atomic::Ordering::Relaxed); } - pub fn set_with_fractional_index(&self, with_fractional_index: bool) { - self.tree_with_fractional_index - .store(with_fractional_index, std::sync::atomic::Ordering::Relaxed); - } - - pub fn set_fractional_index_jitter(&self, jitter: u8) { - self.tree_position_jitter - .store(jitter, std::sync::atomic::Ordering::Relaxed); - } - pub fn merge_interval(&self) -> i64 { self.merge_interval .load(std::sync::atomic::Ordering::Relaxed) @@ -90,7 +65,7 @@ pub struct DefaultRandom; #[cfg(test)] use std::sync::atomic::AtomicU64; use std::sync::{ - atomic::{AtomicBool, AtomicI64, AtomicU8}, + atomic::{AtomicBool, AtomicI64}, Arc, RwLock, }; #[cfg(test)] diff --git a/crates/loro-internal/src/delta/tree.rs b/crates/loro-internal/src/delta/tree.rs index 82974aa5..53722dc5 100644 --- a/crates/loro-internal/src/delta/tree.rs +++ b/crates/loro-internal/src/delta/tree.rs @@ -21,16 +21,21 @@ pub struct TreeDiffItem { #[derive(Debug, Clone, PartialEq)] pub enum TreeExternalDiff { Create { - parent: Option, + parent: TreeParentId, index: usize, position: FractionalIndex, }, Move { - parent: Option, + parent: TreeParentId, index: usize, position: FractionalIndex, + old_parent: TreeParentId, + old_index: usize, + }, + Delete { + old_parent: TreeParentId, + old_index: usize, }, - Delete, } impl TreeDiff { diff --git a/crates/loro-internal/src/diff_calc/tree.rs b/crates/loro-internal/src/diff_calc/tree.rs index d882d21a..3e15f8bb 100644 --- a/crates/loro-internal/src/diff_calc/tree.rs +++ b/crates/loro-internal/src/diff_calc/tree.rs @@ -50,7 +50,7 @@ impl DiffCalculatorTrait for TreeDiffCalculator { fn apply_change( &mut self, - _oplog: &OpLog, + oplog: &OpLog, op: crate::op::RichOp, _vv: Option<&crate::VersionVector>, ) { diff --git a/crates/loro-internal/src/handler.rs b/crates/loro-internal/src/handler.rs index 4018a998..d2804805 100644 --- a/crates/loro-internal/src/handler.rs +++ b/crates/loro-internal/src/handler.rs @@ -11,7 +11,7 @@ use crate::{ diff::{myers_diff, DiffHandler, OperateProxy}, event::{Diff, TextDiffItem}, op::ListSlice, - state::{ContainerState, IndexType, State}, + state::{ContainerState, IndexType, State, TreeParentId}, txn::EventHint, utils::{string_slice::StringSlice, utf16::count_utf16_len}, }; @@ -1171,7 +1171,7 @@ impl Handler { index: _, position, } => { - if let Some(p) = parent.as_mut() { + if let TreeParentId::Node(p) = &mut parent { remap_tree_id(p, container_remap) } remap_tree_id(&mut target, container_remap); @@ -1206,14 +1206,16 @@ impl Handler { mut parent, index: _, position, + old_parent: _, + old_index: _, } => { - if let Some(p) = parent.as_mut() { + if let TreeParentId::Node(p) = &mut parent { 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 => { + TreeExternalDiff::Delete { .. } => { remap_tree_id(&mut target, container_remap); if x.contains(target) { x.delete(target)?; @@ -3939,6 +3941,7 @@ mod test { use super::{HandlerTrait, TextDelta}; use crate::loro::LoroDoc; + use crate::state::TreeParentId; use crate::version::Frontiers; use crate::{fx_map, ToJson}; use loro_common::ID; @@ -4104,7 +4107,7 @@ mod test { loro.set_peer_id(1).unwrap(); let tree = loro.get_tree("root"); let id = loro - .with_txn(|txn| tree.create_with_txn(txn, None, 0)) + .with_txn(|txn| tree.create_with_txn(txn, TreeParentId::Root, 0)) .unwrap(); loro.with_txn(|txn| { let meta = tree.get_meta(id)?; @@ -4134,11 +4137,11 @@ mod test { let tree = loro.get_tree("root"); let text = loro.get_text("text"); loro.with_txn(|txn| { - let id = tree.create_with_txn(txn, None, 0)?; + let id = tree.create_with_txn(txn, TreeParentId::Root, 0)?; let meta = tree.get_meta(id)?; meta.insert_with_txn(txn, "a", 1.into())?; text.insert_with_txn(txn, 0, "abc")?; - let _id2 = tree.create_with_txn(txn, None, 0)?; + let _id2 = tree.create_with_txn(txn, TreeParentId::Root, 0)?; meta.insert_with_txn(txn, "b", 2.into())?; Ok(id) }) diff --git a/crates/loro-internal/src/handler/tree.rs b/crates/loro-internal/src/handler/tree.rs index 4d772c8f..b68a214f 100644 --- a/crates/loro-internal/src/handler/tree.rs +++ b/crates/loro-internal/src/handler/tree.rs @@ -93,8 +93,8 @@ impl TreeInner { self.children_links.get(&parent).map(|x| x.len()) } - fn is_parent(&self, target: TreeID, parent: Option) -> bool { - self.parent_links.get(&target) == Some(&parent) + fn is_parent(&self, target: &TreeID, parent: &Option) -> bool { + self.parent_links.get(target) == Some(parent) } fn get_index_by_tree_id(&self, target: &TreeID) -> Option { @@ -164,7 +164,9 @@ impl HandlerTrait for TreeHandler { let mut q = children .map(|c| { VecDeque::from_iter( - c.iter().enumerate().zip(std::iter::repeat(None::)), + c.iter() + .enumerate() + .zip(std::iter::repeat(TreeParentId::Root)), ) }) .unwrap_or_default(); @@ -179,7 +181,7 @@ impl HandlerTrait for TreeHandler { if let Some(children) = t.value.children_links.get(&Some(*target)) { for (idx, child) in children.iter().enumerate() { - q.push_back(((idx, child), Some(real_id))); + q.push_back(((idx, child), TreeParentId::Node(real_id))); } } } @@ -290,27 +292,25 @@ impl TreeHandler { crate::op::RawOpContent::Tree(Arc::new(TreeOp::Delete { target })), EventHint::Tree(smallvec![TreeDiffItem { target, - action: TreeExternalDiff::Delete, + action: TreeExternalDiff::Delete { + old_parent: self.get_node_parent(&target).unwrap(), + old_index: self.get_index_by_tree_id(&target).unwrap(), + }, }]), &inner.state, ) } - pub fn create>>(&self, parent: T) -> LoroResult { - let parent = parent.into(); - let index: usize = self.children_num(parent).unwrap_or(0); + pub fn create(&self, parent: TreeParentId) -> LoroResult { + let index: usize = self.children_num(&parent).unwrap_or(0); self.create_at(parent, index) } - pub fn create_at>>( - &self, - parent: T, - index: usize, - ) -> LoroResult { + pub fn create_at(&self, parent: TreeParentId, index: usize) -> LoroResult { match &self.inner { MaybeDetached::Detached(t) => { let t = &mut t.try_lock().unwrap().value; - Ok(t.create(parent.into(), index)) + Ok(t.create(parent.tree_id(), index)) } MaybeDetached::Attached(a) => { a.with_txn(|txn| self.create_with_txn(txn, parent, index)) @@ -321,23 +321,33 @@ impl TreeHandler { /// For undo/redo, Specify the TreeID of the created node pub(crate) fn create_at_with_target_for_apply_diff( &self, - parent: Option, + parent: TreeParentId, position: FractionalIndex, target: TreeID, ) -> LoroResult { 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 p { + TreeParentId::Node(p) => { + if self.contains(p) { + return self.move_at_with_target_for_apply_diff(parent, position, target); + } + } + TreeParentId::Root => { + return self.move_at_with_target_for_apply_diff(parent, position, target); + } + TreeParentId::Deleted | TreeParentId::Unexist => {} } } - let with_event = !parent.is_some_and(|p| !self.contains(p)); + let with_event = !parent.tree_id().is_some_and(|p| !self.contains(p)); if !with_event { return Ok(false); } @@ -349,7 +359,7 @@ impl TreeHandler { let index = self .get_index_by_fractional_index( - parent, + &parent, &NodePosition { position: position.clone(), idlp: self.next_idlp(), @@ -365,7 +375,7 @@ impl TreeHandler { inner.container_idx, crate::op::RawOpContent::Tree(Arc::new(TreeOp::Create { target, - parent, + parent: parent.tree_id(), position: position.clone(), })), EventHint::Tree(smallvec![TreeDiffItem { @@ -379,11 +389,13 @@ impl TreeHandler { &inner.state, )?; - Ok(self.children(Some(target)).unwrap_or_default()) + Ok(self + .children(&TreeParentId::Node(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)?; + self.create_at_with_target_for_apply_diff(TreeParentId::Node(target), position, child)?; } Ok(true) } @@ -391,7 +403,7 @@ impl TreeHandler { /// For undo/redo, Specify the TreeID of the created node pub(crate) fn move_at_with_target_for_apply_diff( &self, - parent: Option, + parent: TreeParentId, position: FractionalIndex, target: TreeID, ) -> LoroResult { @@ -412,14 +424,14 @@ impl TreeHandler { let index = self .get_index_by_fractional_index( - parent, + &parent, &NodePosition { position: position.clone(), idlp: self.next_idlp(), }, ) .unwrap_or(0); - let with_event = !parent.is_some_and(|p| !self.contains(p)); + let with_event = !parent.tree_id().is_some_and(|p| !self.contains(p)); if !with_event { return Ok(false); @@ -436,7 +448,7 @@ impl TreeHandler { inner.container_idx, crate::op::RawOpContent::Tree(Arc::new(TreeOp::Move { target, - parent, + parent: parent.tree_id(), position: position.clone(), })), EventHint::Tree(smallvec![TreeDiffItem { @@ -445,6 +457,9 @@ impl TreeHandler { parent, index, position: position.clone(), + // the old parent should be exist, so we can unwrap + old_parent: self.get_node_parent(&target).unwrap(), + old_index: self.get_index_by_tree_id(&target).unwrap(), }, }]), &inner.state, @@ -453,17 +468,16 @@ impl TreeHandler { Ok(true) } - pub(crate) fn create_with_txn>>( + pub(crate) fn create_with_txn( &self, txn: &mut Transaction, - parent: T, + parent: TreeParentId, index: usize, ) -> LoroResult { let inner = self.inner.try_attached_state()?; - let parent: Option = parent.into(); let target = TreeID::from_id(txn.next_id()); - match self.generate_position_at(&target, parent, index) { + match self.generate_position_at(&target, &parent, index) { FractionalIndexGenResult::Ok(position) => { self.create_with_position(inner, txn, target, parent, index, position) } @@ -473,26 +487,25 @@ impl TreeHandler { self.create_with_position(inner, txn, id, parent, index, position)?; continue; } - self.mov_with_position(inner, txn, id, parent, index + i, position)?; + self.mov_with_position(inner, txn, id, parent, index + i, position, index + i)?; } Ok(target) } } } - pub fn mov>>(&self, target: TreeID, parent: T) -> LoroResult<()> { - let parent = parent.into(); + pub fn mov(&self, target: TreeID, parent: TreeParentId) -> LoroResult<()> { match &self.inner { MaybeDetached::Detached(_) => { - let mut index: usize = self.children_num(parent).unwrap_or(0); - if self.is_parent(target, parent) { + let mut index: usize = self.children_num(&parent).unwrap_or(0); + if self.is_parent(&target, &parent) { index -= 1; } self.move_to(target, parent, index) } MaybeDetached::Attached(a) => { - let mut index = self.children_num(parent).unwrap_or(0); - if self.is_parent(target, parent) { + let mut index = self.children_num(&parent).unwrap_or(0); + if self.is_parent(&target, &parent) { index -= 1; } a.with_txn(|txn| self.mov_with_txn(txn, target, parent, index)) @@ -501,11 +514,11 @@ impl TreeHandler { } pub fn mov_after(&self, target: TreeID, other: TreeID) -> LoroResult<()> { - let parent: Option = self + let parent = self .get_node_parent(&other) .ok_or(LoroTreeError::TreeNodeNotExist(other))?; let mut index = self.get_index_by_tree_id(&other).unwrap() + 1; - if self.is_parent(target, parent) && self.get_index_by_tree_id(&target).unwrap() < index { + if self.is_parent(&target, &parent) && self.get_index_by_tree_id(&target).unwrap() < index { index -= 1; } self.move_to(target, parent, index) @@ -516,7 +529,7 @@ impl TreeHandler { .get_node_parent(&other) .ok_or(LoroTreeError::TreeNodeNotExist(other))?; let mut index = self.get_index_by_tree_id(&other).unwrap(); - if self.is_parent(target, parent) + if self.is_parent(&target, &parent) && index > 1 && self.get_index_by_tree_id(&target).unwrap() < index { @@ -525,16 +538,11 @@ impl TreeHandler { self.move_to(target, parent, index) } - pub fn move_to>>( - &self, - target: TreeID, - parent: T, - index: usize, - ) -> LoroResult<()> { + pub fn move_to(&self, target: TreeID, parent: TreeParentId, index: usize) -> LoroResult<()> { match &self.inner { MaybeDetached::Detached(t) => { let mut t = t.try_lock().unwrap(); - t.value.mov(target, parent.into(), index) + t.value.mov(target, parent.tree_id(), index) } MaybeDetached::Attached(a) => { a.with_txn(|txn| self.mov_with_txn(txn, target, parent, index)) @@ -542,19 +550,18 @@ impl TreeHandler { } } - pub(crate) fn mov_with_txn>>( + pub(crate) fn mov_with_txn( &self, txn: &mut Transaction, target: TreeID, - parent: T, + parent: TreeParentId, index: usize, ) -> LoroResult<()> { - let parent = parent.into(); let inner = self.inner.try_attached_state()?; - let mut children_len = self.children_num(parent).unwrap_or(0); + let mut children_len = self.children_num(&parent).unwrap_or(0); let mut already_in_parent = false; // check the input is valid - if self.is_parent(target, parent) { + if self.is_parent(&target, &parent) { // If the position after moving is same as the current position , do nothing if let Some(current_index) = self.get_index_by_tree_id(&target) { if current_index == index { @@ -573,17 +580,18 @@ impl TreeHandler { } .into()); } + let old_index = self.get_index_by_tree_id(&target).unwrap(); if already_in_parent { - self.delete_position(parent, target); + self.delete_position(&parent, &target); } - match self.generate_position_at(&target, parent, index) { + match self.generate_position_at(&target, &parent, index) { FractionalIndexGenResult::Ok(position) => { - self.mov_with_position(inner, txn, target, parent, index, position) + self.mov_with_position(inner, txn, target, parent, index, position, old_index) } FractionalIndexGenResult::Rearrange(ids) => { for (i, (id, position)) in ids.into_iter().enumerate() { - self.mov_with_position(inner, txn, id, parent, index + i, position)?; + self.mov_with_position(inner, txn, id, parent, index + i, position, old_index)?; } Ok(()) } @@ -596,7 +604,7 @@ impl TreeHandler { inner: &BasicHandler, txn: &mut Transaction, tree_id: TreeID, - parent: Option, + parent: TreeParentId, index: usize, position: FractionalIndex, ) -> LoroResult { @@ -604,7 +612,7 @@ impl TreeHandler { inner.container_idx, crate::op::RawOpContent::Tree(Arc::new(TreeOp::Create { target: tree_id, - parent, + parent: parent.tree_id(), position: position.clone(), })), EventHint::Tree(smallvec![TreeDiffItem { @@ -626,15 +634,16 @@ impl TreeHandler { inner: &BasicHandler, txn: &mut Transaction, target: TreeID, - parent: Option, + parent: TreeParentId, index: usize, position: FractionalIndex, + old_index: usize, ) -> LoroResult<()> { txn.apply_local_op( inner.container_idx, crate::op::RawOpContent::Tree(Arc::new(TreeOp::Move { target, - parent, + parent: parent.tree_id(), position: position.clone(), })), EventHint::Tree(smallvec![TreeDiffItem { @@ -643,6 +652,8 @@ impl TreeHandler { parent, index, position, + old_parent: self.get_node_parent(&target).unwrap(), + old_index, }, }]), &inner.state, @@ -671,46 +682,42 @@ impl TreeHandler { } /// Get the parent of the node, if the node is deleted or does not exist, return None - pub fn get_node_parent(&self, target: &TreeID) -> Option> { + pub fn get_node_parent(&self, target: &TreeID) -> Option { match &self.inner { MaybeDetached::Detached(t) => { let t = t.try_lock().unwrap(); - t.value.get_parent(target) + t.value.get_parent(target).map(TreeParentId::from) } MaybeDetached::Attached(a) => a.with_state(|state| { let a = state.as_tree_state().unwrap(); - match a.parent(target) { - TreeParentId::Root => Some(None), - TreeParentId::Node(parent) => Some(Some(parent)), - TreeParentId::Deleted | TreeParentId::Unexist => None, - } + a.parent(target) }), } } // TODO: iterator - pub fn children(&self, parent: Option) -> Option> { + pub fn children(&self, parent: &TreeParentId) -> Option> { match &self.inner { MaybeDetached::Detached(t) => { let t = t.try_lock().unwrap(); - t.value.get_children(parent) + t.value.get_children(parent.tree_id()) } MaybeDetached::Attached(a) => a.with_state(|state| { let a = state.as_tree_state().unwrap(); - a.children(&TreeParentId::from(parent)) + a.children(parent) }), } } - pub fn children_num(&self, parent: Option) -> Option { + pub fn children_num(&self, parent: &TreeParentId) -> Option { match &self.inner { MaybeDetached::Detached(t) => { let t = t.try_lock().unwrap(); - t.value.children_num(parent) + t.value.children_num(parent.tree_id()) } MaybeDetached::Attached(a) => a.with_state(|state| { let a = state.as_tree_state().unwrap(); - a.children_num(&TreeParentId::from(parent)) + a.children_num(parent) }), } } @@ -741,15 +748,15 @@ impl TreeHandler { } } - pub fn is_parent(&self, target: TreeID, parent: Option) -> bool { + pub fn is_parent(&self, target: &TreeID, parent: &TreeParentId) -> bool { match &self.inner { MaybeDetached::Detached(t) => { let t = t.try_lock().unwrap(); - t.value.is_parent(target, parent) + t.value.is_parent(target, &parent.tree_id()) } MaybeDetached::Attached(a) => a.with_state(|state| { let a = state.as_tree_state().unwrap(); - a.is_parent(&TreeParentId::from(parent), &target) + a.is_parent(parent, target) }), } } @@ -768,7 +775,7 @@ impl TreeHandler { } pub fn roots(&self) -> Vec { - self.children(None).unwrap_or_default() + self.children(&TreeParentId::Root).unwrap_or_default() } #[allow(non_snake_case)] @@ -787,7 +794,7 @@ impl TreeHandler { fn generate_position_at( &self, target: &TreeID, - parent: Option, + parent: &TreeParentId, index: usize, ) -> FractionalIndexGenResult { let MaybeDetached::Attached(a) = &self.inner else { @@ -795,7 +802,7 @@ impl TreeHandler { }; a.with_state(|state| { let a = state.as_tree_state_mut().unwrap(); - a.generate_position_at(target, &TreeParentId::from(parent), index) + a.generate_position_at(target, parent, index) }) } @@ -825,20 +832,20 @@ impl TreeHandler { } } - fn delete_position(&self, parent: Option, target: TreeID) { + fn delete_position(&self, parent: &TreeParentId, target: &TreeID) { let MaybeDetached::Attached(a) = &self.inner else { unreachable!() }; a.with_state(|state| { let a = state.as_tree_state_mut().unwrap(); - a.delete_position(&TreeParentId::from(parent), target) + a.delete_position(parent, &target) }) } // use for apply diff pub(crate) fn get_index_by_fractional_index( &self, - parent: Option, + parent: &TreeParentId, node_position: &NodePosition, ) -> Option { match &self.inner { @@ -847,7 +854,7 @@ impl TreeHandler { } MaybeDetached::Attached(a) => a.with_state(|state| { let a = state.as_tree_state().unwrap(); - a.get_index_by_position(&TreeParentId::from(parent), node_position) + a.get_index_by_position(parent, node_position) }), } } @@ -860,4 +867,51 @@ impl TreeHandler { MaybeDetached::Attached(a) => a.with_txn(|txn| Ok(txn.next_idlp())).unwrap(), } } + + pub fn is_fractional_index_enabled(&self) -> bool { + match &self.inner { + MaybeDetached::Detached(_) => { + unreachable!() + } + MaybeDetached::Attached(a) => a.with_state(|state| { + let a = state.as_tree_state().unwrap(); + a.is_fractional_index_enabled() + }), + } + } + + /// Set whether to generate fractional index for Tree Position. The LoroDoc is set to disable fractional index by default. + /// + /// The jitter is used to avoid conflicts when multiple users are creating the node at the same position. + /// value 0 is default, which means no jitter, any value larger than 0 will enable jitter. + /// + /// Generally speaking, jitter will affect the growth rate of document size. + /// [Read more about it](https://www.loro.dev/blog/movable-tree#implementation-and-encoding-size) + pub fn set_enable_fractional_index(&self, jitter: u8) { + match &self.inner { + MaybeDetached::Detached(_) => { + unreachable!() + } + MaybeDetached::Attached(a) => a.with_state(|state| { + let a = state.as_tree_state_mut().unwrap(); + a.enable_generate_fractional_index(jitter); + }), + } + } + + /// Disable the fractional index generation for Tree Position when + /// you don't need the Tree's siblings to be sorted. The fractional index will be always default. + /// + /// The LoroDoc is set to disable fractional index by default. + pub fn set_disable_fractional_index(&self) { + match &self.inner { + MaybeDetached::Detached(_) => { + unreachable!() + } + MaybeDetached::Attached(a) => a.with_state(|state| { + let a = state.as_tree_state_mut().unwrap(); + a.disable_generate_fractional_index(); + }), + } + } } diff --git a/crates/loro-internal/src/lib.rs b/crates/loro-internal/src/lib.rs index 83a8ade8..1edcdfed 100644 --- a/crates/loro-internal/src/lib.rs +++ b/crates/loro-internal/src/lib.rs @@ -21,6 +21,7 @@ pub use loro::LoroDoc; pub use loro_common; pub use oplog::OpLog; pub use state::DocState; +pub use state::TreeParentId; pub use undo::UndoManager; pub mod awareness; pub mod cursor; diff --git a/crates/loro-internal/src/loro.rs b/crates/loro-internal/src/loro.rs index bb62350c..445651f3 100644 --- a/crates/loro-internal/src/loro.rs +++ b/crates/loro-internal/src/loro.rs @@ -31,7 +31,7 @@ use crate::{ decode_snapshot, export_fast_snapshot, export_snapshot, json_schema::json::JsonSchema, parse_header_and_body, EncodeMode, ParsedHeaderAndBody, }, - event::{str_to_path, EventTriggerKind, Index, InternalDocDiff, Path}, + event::{str_to_path, EventTriggerKind, Index, InternalDocDiff}, handler::{Handler, MovableListHandler, TextHandler, TreeHandler, ValueOrHandler}, id::PeerID, obs::{Observer, SubID, Subscriber}, @@ -172,22 +172,6 @@ impl LoroDoc { self.config.set_merge_interval(interval); } - /// Set whether to use fractional index for Tree Position. - #[inline] - pub fn set_with_fractional_index(&self, with_fractional_index: bool) { - self.config.set_with_fractional_index(with_fractional_index); - } - - /// Set the jitter of the tree position(Fractional Index). - /// - /// The jitter is used to avoid conflicts when multiple users are creating the node at the same position. - /// value 0 is default, which means no jitter, any value larger than 0 will enable jitter. - /// Generally speaking, jitter will affect the growth rate of document size. - #[inline] - pub fn set_fractional_index_jitter(&self, jitter: u8) { - self.config.set_fractional_index_jitter(jitter); - } - #[inline] pub fn config_text_style(&self, text_style: StyleConfigMap) { *self.config.text_style_config.try_write().unwrap() = text_style; diff --git a/crates/loro-internal/src/oplog.rs b/crates/loro-internal/src/oplog.rs index ed70e433..023b0163 100644 --- a/crates/loro-internal/src/oplog.rs +++ b/crates/loro-internal/src/oplog.rs @@ -7,7 +7,7 @@ use std::cell::RefCell; use std::cmp::Ordering; use std::rc::Rc; use std::sync::Mutex; -use tracing::{debug, instrument, trace, trace_span}; +use tracing::{debug, trace, trace_span}; use self::change_store::iter::MergedChangeIter; use self::pending_changes::PendingChanges; diff --git a/crates/loro-internal/src/oplog/change_store.rs b/crates/loro-internal/src/oplog/change_store.rs index df2184b6..34c83730 100644 --- a/crates/loro-internal/src/oplog/change_store.rs +++ b/crates/loro-internal/src/oplog/change_store.rs @@ -1320,8 +1320,8 @@ impl ChangesBlockBytes { #[cfg(test)] mod test { use crate::{ - oplog::convert_change_to_remote, ListHandler, LoroDoc, MovableListHandler, TextHandler, - TreeHandler, + oplog::convert_change_to_remote, state::TreeParentId, ListHandler, LoroDoc, + MovableListHandler, TextHandler, TreeHandler, }; use super::*; @@ -1399,10 +1399,10 @@ mod test { let tree = map .insert_container("tree", TreeHandler::new_detached()) .unwrap(); - let node_id = tree.create(None)?; + let node_id = tree.create(TreeParentId::Root)?; tree.get_meta(node_id)?.insert("key", "value")?; - let node_b = tree.create(None)?; - tree.move_to(node_b, None, 0).unwrap(); + let node_b = tree.create(TreeParentId::Root)?; + tree.move_to(node_b, TreeParentId::Root, 0).unwrap(); let movable_list = map .insert_container("movable", MovableListHandler::new_detached()) diff --git a/crates/loro-internal/src/state.rs b/crates/loro-internal/src/state.rs index 2b252f71..6575efab 100644 --- a/crates/loro-internal/src/state.rs +++ b/crates/loro-internal/src/state.rs @@ -1,9 +1,8 @@ use std::{ borrow::Cow, - collections::BTreeMap, io::Write, sync::{ - atomic::{AtomicBool, AtomicU64, AtomicU8, Ordering}, + atomic::{AtomicU64, Ordering}, Arc, Mutex, RwLock, Weak, }, }; @@ -15,7 +14,7 @@ use fxhash::{FxHashMap, FxHashSet}; use itertools::Itertools; use loro_common::{ContainerID, LoroError, LoroResult}; use loro_delta::DeltaItem; -use tracing::{info_span, instrument, trace}; +use tracing::{info_span, instrument}; use crate::{ configure::{Configure, DefaultRandom, SecureRandomGenerator}, @@ -49,9 +48,8 @@ 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, NodePosition, TreeParentId, TreeState, -}; +pub use tree_state::TreeParentId; +pub(crate) use tree_state::{get_meta_value, FractionalIndexGenResult, NodePosition, TreeState}; use self::{container_store::ContainerWrapper, unknown_state::UnknownState}; @@ -310,18 +308,8 @@ impl State { Self::RichtextState(Box::new(RichtextState::new(idx, config))) } - pub fn new_tree( - idx: ContainerIdx, - peer: PeerID, - jitter: Arc, - with_fractional_index: Arc, - ) -> Self { - Self::TreeState(Box::new(TreeState::new( - idx, - peer, - jitter, - with_fractional_index, - ))) + pub fn new_tree(idx: ContainerIdx, peer: PeerID) -> Self { + Self::TreeState(Box::new(TreeState::new(idx, peer))) } pub fn new_unknown(idx: ContainerIdx) -> Self { @@ -1482,12 +1470,7 @@ fn create_state_(idx: ContainerIdx, config: &Configure, peer: u64) -> State { idx, config.text_style_config.clone(), ))), - ContainerType::Tree => State::TreeState(Box::new(TreeState::new( - idx, - peer, - config.tree_position_jitter.clone(), - config.tree_with_fractional_index.clone(), - ))), + ContainerType::Tree => State::TreeState(Box::new(TreeState::new(idx, peer))), ContainerType::MovableList => State::MovableListState(Box::new(MovableListState::new(idx))), #[cfg(feature = "counter")] ContainerType::Counter => { diff --git a/crates/loro-internal/src/state/container_store.rs b/crates/loro-internal/src/state/container_store.rs index e0f8d75d..eb4b4707 100644 --- a/crates/loro-internal/src/state/container_store.rs +++ b/crates/loro-internal/src/state/container_store.rs @@ -1,16 +1,12 @@ -#[cfg(feature = "counter")] -use super::counter_state::CounterState; -use super::{ContainerCreationContext, MovableListState, State, TreeState}; +use super::{ContainerCreationContext, State}; use crate::{ arena::SharedArena, configure::Configure, container::idx::ContainerIdx, - state::{FastStateSnapshot, RichtextState}, }; use bytes::Bytes; -use fxhash::FxHashMap; use inner_store::InnerStore; -use loro_common::{ContainerID, ContainerType, LoroResult, LoroValue}; +use loro_common::{LoroResult, LoroValue}; use std::sync::{atomic::AtomicU64, Arc}; pub(crate) use container_wrapper::ContainerWrapper; @@ -443,7 +439,7 @@ mod encode { #[cfg(test)] mod test { use super::*; - use crate::{ListHandler, LoroDoc, MapHandler, MovableListHandler}; + use crate::{state::TreeParentId, ListHandler, LoroDoc, MapHandler, MovableListHandler}; fn decode_container_store(bytes: Bytes) -> ContainerStore { let mut new_store = ContainerStore::new( @@ -468,8 +464,8 @@ mod test { list.push("item1").unwrap(); let tree = doc.get_tree("tree"); - let root = tree.create(None).unwrap(); - tree.create_at(Some(root), 0).unwrap(); + let root = tree.create(TreeParentId::Root).unwrap(); + tree.create_at(TreeParentId::Node(root), 0).unwrap(); let movable_list = doc.get_movable_list("movable_list"); movable_list.insert(0, "movable_item").unwrap(); diff --git a/crates/loro-internal/src/state/movable_list_state.rs b/crates/loro-internal/src/state/movable_list_state.rs index 95746049..51dd1247 100644 --- a/crates/loro-internal/src/state/movable_list_state.rs +++ b/crates/loro-internal/src/state/movable_list_state.rs @@ -18,8 +18,7 @@ use crate::{ handler::ValueOrHandler, op::{ListSlice, Op, RawOp}, state::movable_list_state::inner::PushElemInfo, - txn::Transaction, - ApplyDiff, DocState, ListDiff, + txn::Transaction, DocState, ListDiff, }; use self::{ diff --git a/crates/loro-internal/src/state/tree_state.rs b/crates/loro-internal/src/state/tree_state.rs index 45091428..3c64f48b 100644 --- a/crates/loro-internal/src/state/tree_state.rs +++ b/crates/loro-internal/src/state/tree_state.rs @@ -13,7 +13,6 @@ use serde::Serialize; use std::collections::VecDeque; use std::fmt::Debug; use std::ops::{Deref, DerefMut}; -use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use std::sync::{Arc, Mutex, Weak}; use super::{ContainerState, DiffApplyContext}; @@ -33,6 +32,16 @@ use crate::{ op::RawOp, }; +#[derive(Clone, Debug, Default, EnumAsInner)] +pub enum TreeFractionalIndexConfigInner { + GenerateFractionalIndex { + jitter: u8, + rng: Box, + }, + #[default] + AlwaysDefault, +} + /// The state of movable tree. /// /// using flat representation @@ -41,10 +50,8 @@ pub struct TreeState { idx: ContainerIdx, trees: FxHashMap, children: TreeChildrenCache, - /// Whether the tree has fractional index. If false, the fractional index is always [`FractionalIndex::default`] - with_fractional_index: Arc, - rng: Option, - jitter: Arc, + fractional_index_config: TreeFractionalIndexConfigInner, + peer_id: PeerID, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -88,8 +95,28 @@ impl From> for TreeParentId { } } +impl From for TreeParentId { + fn from(id: TreeID) -> Self { + if TreeID::is_deleted_root(&id) { + TreeParentId::Deleted + } else { + TreeParentId::Node(id) + } + } +} + +impl From<&TreeID> for TreeParentId { + fn from(id: &TreeID) -> Self { + if TreeID::is_deleted_root(id) { + TreeParentId::Deleted + } else { + TreeParentId::Node(*id) + } + } +} + impl TreeParentId { - fn id(&self) -> Option { + pub fn tree_id(&self) -> Option { match self { TreeParentId::Node(id) => Some(*id), TreeParentId::Root => None, @@ -599,22 +626,13 @@ impl NodePosition { } impl TreeState { - pub fn new( - idx: ContainerIdx, - peer_id: PeerID, - jitter_config: Arc, - with_fractional_index: Arc, - ) -> Self { - let jitter = jitter_config.load(Ordering::Relaxed); - let use_jitter = jitter != 1; - + pub fn new(idx: ContainerIdx, peer_id: PeerID) -> Self { Self { idx, trees: FxHashMap::default(), children: Default::default(), - with_fractional_index, - rng: use_jitter.then_some(rand::rngs::StdRng::seed_from_u64(peer_id)), - jitter: jitter_config, + fractional_index_config: TreeFractionalIndexConfigInner::default(), + peer_id, } } @@ -638,7 +656,7 @@ impl TreeState { } if let Some(old_parent) = self.trees.get(&target).map(|x| x.parent) { // remove old position - self.delete_position(&old_parent, target); + self.delete_position(&old_parent, &target); } let entry = self.children.entry(parent).or_default(); @@ -686,11 +704,8 @@ impl TreeState { } /// Get the parent of the node, if the node is deleted or does not exist, return None - pub fn parent(&self, target: &TreeID) -> TreeParentId { - self.trees - .get(target) - .map(|x| x.parent) - .unwrap_or(TreeParentId::Unexist) + pub fn parent(&self, target: &TreeID) -> Option { + self.trees.get(target).map(|x| x.parent) } /// If the node exists and is not deleted, return false. @@ -720,7 +735,11 @@ impl TreeState { let children = self.children.get(&root); let mut q = children .map(|x| { - VecDeque::from_iter(x.iter().enumerate().zip(std::iter::repeat(None::))) + VecDeque::from_iter( + x.iter() + .enumerate() + .zip(std::iter::repeat(TreeParentId::Root)), + ) }) .unwrap_or_default(); @@ -737,7 +756,7 @@ impl TreeState { .iter() .enumerate() .map(|(index, (position, this_target))| { - ((index, (position, this_target)), Some(target)) + ((index, (position, this_target)), TreeParentId::Node(target)) }), ); } @@ -758,7 +777,7 @@ impl TreeState { for (index, (position, target)) in children.iter().enumerate() { ans.push(TreeNode { id: *target, - parent: root.id(), + parent: root, position: position.position.clone(), index, }); @@ -815,9 +834,9 @@ impl TreeState { } /// Delete the position cache of the node - pub(crate) fn delete_position(&mut self, parent: &TreeParentId, target: TreeID) { + pub(crate) fn delete_position(&mut self, parent: &TreeParentId, target: &TreeID) { if let Some(x) = self.children.get_mut(parent) { - x.delete_child(&target); + x.delete_child(target); } } @@ -827,21 +846,49 @@ impl TreeState { parent: &TreeParentId, index: usize, ) -> FractionalIndexGenResult { - if !self.with_fractional_index.load(Ordering::Relaxed) { - return FractionalIndexGenResult::Ok(FractionalIndex::default()); + match &mut self.fractional_index_config { + TreeFractionalIndexConfigInner::GenerateFractionalIndex { jitter, rng } => { + if *jitter == 0 { + self.children + .entry(*parent) + .or_default() + .generate_fi_at(index, target) + } else { + self.children + .entry(*parent) + .or_default() + .generate_fi_at_jitter(index, target, rng.as_mut(), *jitter) + } + } + TreeFractionalIndexConfigInner::AlwaysDefault => { + FractionalIndexGenResult::Ok(FractionalIndex::default()) + } } + } - if let Some(rng) = self.rng.as_mut() { - self.children - .entry(*parent) - .or_default() - .generate_fi_at_jitter(index, target, rng, self.jitter.load(Ordering::Relaxed)) - } else { - self.children - .entry(*parent) - .or_default() - .generate_fi_at(index, target) + pub(crate) fn is_fractional_index_enabled(&self) -> bool { + !matches!( + self.fractional_index_config, + TreeFractionalIndexConfigInner::AlwaysDefault + ) + } + + pub(crate) fn enable_generate_fractional_index(&mut self, jitter: u8) { + if let TreeFractionalIndexConfigInner::GenerateFractionalIndex { + jitter: old_jitter, .. + } = &mut self.fractional_index_config + { + *old_jitter = jitter; + return; } + self.fractional_index_config = TreeFractionalIndexConfigInner::GenerateFractionalIndex { + jitter, + rng: Box::new(rand::rngs::StdRng::seed_from_u64(self.peer_id)), + }; + } + + pub(crate) fn disable_generate_fractional_index(&mut self) { + self.fractional_index_config = TreeFractionalIndexConfigInner::AlwaysDefault; } pub(crate) fn get_position(&self, target: &TreeID) -> Option { @@ -849,7 +896,7 @@ impl TreeState { } pub(crate) fn get_index_by_tree_id(&self, target: &TreeID) -> Option { - let parent = self.parent(target); + let parent = self.parent(target)?; (!parent.is_deleted()) .then(|| { self.children @@ -922,13 +969,15 @@ impl ContainerState for TreeState { ans.push(TreeDiffItem { target, action: TreeExternalDiff::Create { - parent: parent.into_node().ok(), + parent: *parent, index, position: position.clone(), }, }); } TreeInternalDiff::Move { parent, position } => { + let old_parent = self.trees.get(&target).unwrap().parent; + let old_index = self.get_index_by_tree_id(&target).unwrap(); if need_check { let was_alive = !self.is_node_deleted(&target); if self @@ -940,7 +989,10 @@ impl ContainerState for TreeState { // delete event ans.push(TreeDiffItem { target, - action: TreeExternalDiff::Delete, + action: TreeExternalDiff::Delete { + old_parent, + old_index, + }, }); } // Otherwise, it's a normal move inside deleted nodes, no event is needed @@ -949,9 +1001,11 @@ impl ContainerState for TreeState { ans.push(TreeDiffItem { target, action: TreeExternalDiff::Move { - parent: parent.into_node().ok(), + parent: *parent, index: self.get_index_by_tree_id(&target).unwrap(), position: position.clone(), + old_parent, + old_index, }, }); } else { @@ -959,7 +1013,7 @@ impl ContainerState for TreeState { ans.push(TreeDiffItem { target, action: TreeExternalDiff::Create { - parent: parent.into_node().ok(), + parent: *parent, index: self.get_index_by_tree_id(&target).unwrap(), position: position.clone(), }, @@ -974,9 +1028,11 @@ impl ContainerState for TreeState { ans.push(TreeDiffItem { target, action: TreeExternalDiff::Move { - parent: parent.into_node().ok(), + parent: *parent, index, position: position.clone(), + old_parent, + old_index, }, }); }; @@ -986,15 +1042,17 @@ impl ContainerState for TreeState { if need_check && self.is_node_deleted(&target) { send_event = false; } - - self.mov(target, *parent, last_move_op, position.clone(), false) - .unwrap(); if send_event { ans.push(TreeDiffItem { target, - action: TreeExternalDiff::Delete, + action: TreeExternalDiff::Delete { + old_parent: self.trees.get(&target).unwrap().parent, + old_index: self.get_index_by_tree_id(&target).unwrap(), + }, }); } + self.mov(target, *parent, last_move_op, position.clone(), false) + .unwrap(); } TreeInternalDiff::MoveInDelete { parent, position } => { self.mov(target, *parent, last_move_op, position.clone(), false) @@ -1005,7 +1063,10 @@ impl ContainerState for TreeState { if !self.is_node_deleted(&target) { ans.push(TreeDiffItem { target, - action: TreeExternalDiff::Delete, + action: TreeExternalDiff::Delete { + old_parent: self.trees.get(&target).unwrap().parent, + old_index: self.get_index_by_tree_id(&target).unwrap(), + }, }); } // delete it from state @@ -1041,7 +1102,9 @@ impl ContainerState for TreeState { // create associated metadata container match &diff.action { TreeInternalDiff::Create { parent, position } - | TreeInternalDiff::Move { parent, position } => { + | TreeInternalDiff::Move { + parent, position, .. + } => { if need_check { self.mov(target, *parent, last_move_op, Some(position.clone()), true) .unwrap_or_default(); @@ -1130,7 +1193,7 @@ impl ContainerState for TreeState { let diff = TreeDiffItem { target: *node, action: TreeExternalDiff::Create { - parent: node_parent.into_node().ok(), + parent: node_parent, index, position: position.position.clone(), }, @@ -1241,7 +1304,7 @@ pub(crate) fn get_meta_value(nodes: &mut Vec, state: &mut DocState) { pub(crate) struct TreeNode { pub(crate) id: TreeID, - pub(crate) parent: Option, + pub(crate) parent: TreeParentId, pub(crate) position: FractionalIndex, pub(crate) index: usize, } @@ -1252,6 +1315,7 @@ impl TreeNode { t.insert("id".to_string(), self.id.to_string().into()); let p = self .parent + .tree_id() .map(|p| p.to_string().into()) .unwrap_or(LoroValue::Null); t.insert("parent".to_string(), p); @@ -1431,16 +1495,12 @@ mod snapshot { let n = state.trees.get(&node.id).unwrap(); let last_set_id = n.last_move_op; nodes.push(EncodedTreeNode { - parent_idx_plus_two: node - .parent - .map(|p| { - if p.is_deleted_root() { - 1 - } else { - id_to_idx.get(&p).unwrap() + 2 - } - }) - .unwrap_or(0), + parent_idx_plus_two: match node.parent { + TreeParentId::Deleted => 1, + TreeParentId::Root => 0, + TreeParentId::Node(id) => id_to_idx.get(&id).unwrap() + 2, + TreeParentId::Unexist => unreachable!(), + }, last_set_peer_idx: peers.register(&last_set_id.peer), last_set_counter: last_set_id.counter, last_set_lamport: last_set_id.lamport, @@ -1496,12 +1556,7 @@ mod snapshot { peers.push(PeerID::from_le_bytes(buf)); } - let mut tree = TreeState::new( - idx, - ctx.peer, - ctx.configure.tree_position_jitter.clone(), - ctx.configure.tree_with_fractional_index.clone(), - ); + let mut tree = TreeState::new(idx, ctx.peer); let encoded: EncodedTree = serde_columnar::from_bytes(bytes)?; let fractional_indexes = PositionArena::decode(&encoded.fractional_indexes).unwrap(); let fractional_indexes = fractional_indexes.parse_to_positions(); @@ -1556,10 +1611,10 @@ mod snapshot { doc.set_peer_id(0).unwrap(); doc.start_auto_commit(); let tree = doc.get_tree("tree"); - let a = tree.create(None).unwrap(); - let b = tree.create(None).unwrap(); - let _c = tree.create(None).unwrap(); - tree.mov(b, a).unwrap(); + let a = tree.create(TreeParentId::Root).unwrap(); + let b = tree.create(TreeParentId::Root).unwrap(); + let _c = tree.create(TreeParentId::Root).unwrap(); + tree.mov(b, TreeParentId::Node(a)).unwrap(); let (bytes, value) = { let mut doc_state = doc.app_state().lock().unwrap(); let tree_state = doc_state.get_tree("tree").unwrap(); diff --git a/crates/loro-internal/src/utils/kv_wrapper.rs b/crates/loro-internal/src/utils/kv_wrapper.rs index af7cfbe1..17a27dd8 100644 --- a/crates/loro-internal/src/utils/kv_wrapper.rs +++ b/crates/loro-internal/src/utils/kv_wrapper.rs @@ -1,12 +1,6 @@ -use std::{ - collections::BTreeMap, - ops::Bound, - sync::{Arc, Mutex}, -}; +use std::sync::{Arc, Mutex}; use bytes::Bytes; -use fxhash::FxHashMap; -use loro_common::ContainerID; use loro_kv_store::MemKvStore; use crate::kv_store::KvStore; diff --git a/crates/loro-internal/src/value.rs b/crates/loro-internal/src/value.rs index f9e1624f..8988eaa3 100644 --- a/crates/loro-internal/src/value.rs +++ b/crates/loro-internal/src/value.rs @@ -558,8 +558,12 @@ pub mod wasm { position, } => { js_sys::Reflect::set(&obj, &"action".into(), &"create".into()).unwrap(); - js_sys::Reflect::set(&obj, &"parent".into(), &JsValue::from(*parent)) - .unwrap(); + js_sys::Reflect::set( + &obj, + &"parent".into(), + &JsValue::from(parent.tree_id()), + ) + .unwrap(); js_sys::Reflect::set(&obj, &"index".into(), &(*index).into()).unwrap(); js_sys::Reflect::set( &obj, @@ -568,18 +572,34 @@ pub mod wasm { ) .unwrap(); } - TreeExternalDiff::Delete { .. } => { + TreeExternalDiff::Delete { + old_parent, + old_index, + } => { js_sys::Reflect::set(&obj, &"action".into(), &"delete".into()).unwrap(); + js_sys::Reflect::set( + &obj, + &"old_parent".into(), + &JsValue::from(old_parent.tree_id()), + ) + .unwrap(); + js_sys::Reflect::set(&obj, &"old_index".into(), &(*old_index).into()) + .unwrap(); } TreeExternalDiff::Move { parent, index, position, - .. + old_parent, + old_index, } => { js_sys::Reflect::set(&obj, &"action".into(), &"move".into()).unwrap(); - js_sys::Reflect::set(&obj, &"parent".into(), &JsValue::from(*parent)) - .unwrap(); + js_sys::Reflect::set( + &obj, + &"parent".into(), + &JsValue::from(parent.tree_id()), + ) + .unwrap(); js_sys::Reflect::set(&obj, &"index".into(), &(*index).into()).unwrap(); js_sys::Reflect::set( &obj, @@ -587,6 +607,14 @@ pub mod wasm { &position.to_string().into(), ) .unwrap(); + js_sys::Reflect::set( + &obj, + &"old_parent".into(), + &JsValue::from(old_parent.tree_id()), + ) + .unwrap(); + js_sys::Reflect::set(&obj, &"old_index".into(), &(*old_index).into()) + .unwrap(); } } array.push(&obj); diff --git a/crates/loro-internal/tests/test.rs b/crates/loro-internal/tests/test.rs index a499c0a4..8ff83b3e 100644 --- a/crates/loro-internal/tests/test.rs +++ b/crates/loro-internal/tests/test.rs @@ -8,6 +8,7 @@ use loro_internal::{ handler::{Handler, TextDelta, ValueOrHandler}, version::Frontiers, ApplyDiff, HandlerTrait, ListHandler, LoroDoc, MapHandler, TextHandler, ToJson, TreeHandler, + TreeParentId, }; use serde_json::json; @@ -724,11 +725,11 @@ fn tree_checkout() { doc_a.subscribe_root(Arc::new(|_e| {})); doc_a.set_peer_id(1).unwrap(); let tree = doc_a.get_tree("root"); - let id1 = tree.create(None).unwrap(); - let id2 = tree.create(id1).unwrap(); + let id1 = tree.create(TreeParentId::Root).unwrap(); + let id2 = tree.create(TreeParentId::Node(id1)).unwrap(); let v1_state = tree.get_deep_value(); let v1 = doc_a.oplog_frontiers(); - let _id3 = tree.create(id2).unwrap(); + let _id3 = tree.create(TreeParentId::Node(id2)).unwrap(); let v2_state = tree.get_deep_value(); let v2 = doc_a.oplog_frontiers(); tree.delete(id2).unwrap(); @@ -757,7 +758,7 @@ fn tree_checkout() { ); doc_a.attach(); - tree.create(None).unwrap(); + tree.create(TreeParentId::Root).unwrap(); } #[test] @@ -853,8 +854,8 @@ fn missing_event_when_checkout() { let doc2 = LoroDoc::new_auto_commit(); let tree = doc2.get_tree("tree"); - let node = tree.create_at(None, 0).unwrap(); - let _ = tree.create_at(None, 0).unwrap(); + let node = tree.create_at(TreeParentId::Root, 0).unwrap(); + let _ = tree.create_at(TreeParentId::Root, 0).unwrap(); let meta = tree.get_meta(node).unwrap(); meta.insert("a", 0).unwrap(); doc.import(&doc2.export_from(&doc.oplog_vv())).unwrap(); @@ -930,7 +931,7 @@ fn insert_attach_container() -> LoroResult<()> { #[test] fn tree_attach() { let tree = TreeHandler::new_detached(); - let id = tree.create(None).unwrap(); + let id = tree.create(TreeParentId::Root).unwrap(); tree.get_meta(id).unwrap().insert("key", "value").unwrap(); let doc = LoroDoc::new_auto_commit(); doc.get_list("list").insert_container(0, tree).unwrap(); diff --git a/crates/loro-internal/tests/tree.rs b/crates/loro-internal/tests/tree.rs new file mode 100644 index 00000000..20edb584 --- /dev/null +++ b/crates/loro-internal/tests/tree.rs @@ -0,0 +1,25 @@ +use loro_internal::{LoroDoc, TreeParentId}; + +#[test] +fn tree_index() { + let doc = LoroDoc::new_auto_commit(); + doc.set_peer_id(0).unwrap(); + let tree = doc.get_tree("tree"); + let root = tree.create(TreeParentId::Root).unwrap(); + let child = tree.create(root.into()).unwrap(); + let child2 = tree.create_at(root.into(), 0).unwrap(); + // sort with OpID + assert_eq!(tree.get_index_by_tree_id(&child).unwrap(), 0); + assert_eq!(tree.get_index_by_tree_id(&child2).unwrap(), 1); + + let doc = LoroDoc::new_auto_commit(); + doc.set_peer_id(0).unwrap(); + let tree = doc.get_tree("tree"); + tree.set_enable_fractional_index(0); + let root = tree.create(TreeParentId::Root).unwrap(); + let child = tree.create(root.into()).unwrap(); + let child2 = tree.create_at(root.into(), 0).unwrap(); + // sort with fractional index + assert_eq!(tree.get_index_by_tree_id(&child).unwrap(), 1); + assert_eq!(tree.get_index_by_tree_id(&child2).unwrap(), 0); +} diff --git a/crates/loro-wasm/src/lib.rs b/crates/loro-wasm/src/lib.rs index 8f7d9f16..f463cd5c 100644 --- a/crates/loro-wasm/src/lib.rs +++ b/crates/loro-wasm/src/lib.rs @@ -24,7 +24,8 @@ use loro_internal::{ undo::{UndoItemMeta, UndoOrRedo}, version::Frontiers, ContainerType, DiffEvent, FxHashMap, HandlerTrait, LoroDoc as LoroDocInner, LoroValue, - MovableListHandler, UndoManager as InnerUndoManager, VersionVector as InternalVersionVector, + MovableListHandler, TreeParentId, UndoManager as InnerUndoManager, + VersionVector as InternalVersionVector, }; use rle::HasLength; use serde::{Deserialize, Serialize}; @@ -340,16 +341,6 @@ impl LoroDoc { self.0.set_change_merge_interval(interval as i64); } - /// Set the jitter of the tree position(Fractional Index). - /// - /// The jitter is used to avoid conflicts when multiple users are creating the node at the same position. - /// value 0 is default, which means no jitter, any value larger than 0 will enable jitter. - /// Generally speaking, jitter will affect the growth rate of document size. - #[wasm_bindgen(js_name = "setFractionalIndexJitter")] - pub fn set_fractional_index_jitter(&self, jitter: u8) { - self.0.set_fractional_index_jitter(jitter); - } - /// Set the rich text format configuration of the document. /// /// You need to config it if you use rich text `mark` method. @@ -3043,9 +3034,9 @@ impl LoroTreeNode { #[wasm_bindgen(js_name = "createNode", skip_typescript)] pub fn create_node(&self, index: Option) -> JsResult { let id = if let Some(index) = index { - self.tree.create_at(Some(self.id), index)? + self.tree.create_at(TreeParentId::Node(self.id), index)? } else { - self.tree.create(Some(self.id))? + self.tree.create(TreeParentId::Node(self.id))? }; let node = LoroTreeNode::from_tree(id, self.tree.clone(), self.doc.clone()); Ok(node) @@ -3075,9 +3066,10 @@ impl LoroTreeNode { pub fn mov(&self, parent: &JsTreeNodeOrUndefined, index: Option) -> JsResult<()> { let parent: Option = parse_js_tree_node(parent)?; if let Some(index) = index { - self.tree.move_to(self.id, parent.map(|x| x.id), index)? + self.tree + .move_to(self.id, parent.map(|x| x.id).into(), index)? } else { - self.tree.mov(self.id, parent.map(|x| x.id))?; + self.tree.mov(self.id, parent.map(|x| x.id).into())?; } Ok(()) @@ -3169,9 +3161,15 @@ impl LoroTreeNode { /// - The object returned is a new js object each time because it need to cross /// the WASM boundary. #[wasm_bindgen] - pub fn parent(&self) -> Option { - let parent = self.tree.get_node_parent(&self.id).flatten(); - parent.map(|p| LoroTreeNode::from_tree(p, self.tree.clone(), self.doc.clone())) + pub fn parent(&self) -> JsResult> { + let parent = self + .tree + .get_node_parent(&self.id) + .ok_or(JsValue::from_str(&format!("TreeID({}) not found", self.id)))?; + let ans = parent + .tree_id() + .map(|p| LoroTreeNode::from_tree(p, self.tree.clone(), self.doc.clone())); + Ok(ans) } /// Get the children of this node. @@ -3180,7 +3178,7 @@ impl LoroTreeNode { /// the WASM boundary. #[wasm_bindgen(skip_typescript)] pub fn children(&self) -> JsValue { - let Some(children) = self.tree.children(Some(self.id)) else { + let Some(children) = self.tree.children(&TreeParentId::Node(self.id)) else { return JsValue::undefined(); }; let children = children.into_iter().map(|c| { @@ -3236,9 +3234,9 @@ impl LoroTree { ) -> JsResult { let parent: Option = parse_js_parent(parent)?; let id = if let Some(index) = index { - self.handler.create_at(parent, index)? + self.handler.create_at(parent.into(), index)? } else { - self.handler.create(parent)? + self.handler.create(parent.into())? }; let node = LoroTreeNode::from_tree(id, self.handler.clone(), self.doc.clone()); Ok(node) @@ -3272,9 +3270,9 @@ impl LoroTree { let parent = parse_js_parent(parent)?; if let Some(index) = index { - self.handler.move_to(target, parent, index)? + self.handler.move_to(target, parent.into(), index)? } else { - self.handler.mov(target, parent)? + self.handler.mov(target, parent.into())? }; Ok(()) @@ -3533,6 +3531,25 @@ impl LoroTree { JsValue::UNDEFINED.into() } } + + /// Set whether to generate fractional index for Tree Position. + /// + /// The jitter is used to avoid conflicts when multiple users are creating the node at the same position. + /// value 0 is default, which means no jitter, any value larger than 0 will enable jitter. + /// + /// Generally speaking, jitter will affect the growth rate of document size. + /// [Read more about it](https://www.loro.dev/blog/movable-tree#implementation-and-encoding-size) + #[wasm_bindgen(js_name = "setEnableFractionalIndex")] + pub fn set_enable_fractional_index(&self, jitter: u8) { + self.handler.set_enable_fractional_index(jitter); + } + + /// Disable the fractional index generation for Tree Position when + /// you don't need the Tree's siblings to be sorted. The fractional index will be always default. + #[wasm_bindgen(js_name = "setDisableFractionalIndex")] + pub fn set_disable_fractional_index(&self) { + self.handler.set_disable_fractional_index(); + } } impl Default for LoroTree { diff --git a/crates/loro/src/change_meta.rs b/crates/loro/src/change_meta.rs index 00f5b05f..b5c5fde7 100644 --- a/crates/loro/src/change_meta.rs +++ b/crates/loro/src/change_meta.rs @@ -2,7 +2,7 @@ use std::{cmp::Ordering, sync::Arc}; use loro_internal::{ change::{Change, Lamport, Timestamp}, - id::{Counter, ID}, + id::ID, version::Frontiers, }; diff --git a/crates/loro/src/lib.rs b/crates/loro/src/lib.rs index f9fe2758..8358a5b0 100644 --- a/crates/loro/src/lib.rs +++ b/crates/loro/src/lib.rs @@ -12,11 +12,12 @@ use loro_internal::cursor::Side; use loro_internal::encoding::ImportBlobMetadata; use loro_internal::handler::HandlerTrait; use loro_internal::handler::ValueOrHandler; -use loro_internal::json::JsonChange; +use loro_internal::loro_common::LoroTreeError; use loro_internal::undo::{OnPop, OnPush}; use loro_internal::DocState; use loro_internal::LoroDoc as InnerLoroDoc; use loro_internal::OpLog; +use loro_internal::TreeParentId; use loro_internal::{ handler::Handler as InnerHandler, ListHandler as InnerListHandler, MapHandler as InnerMapHandler, MovableListHandler as InnerMovableListHandler, @@ -155,22 +156,6 @@ impl LoroDoc { self.doc.set_change_merge_interval(interval); } - /// Set whether to use fractional index for Tree Position. - #[inline] - pub fn set_with_fractional_index(&self, with_fractional_index: bool) { - self.doc.set_with_fractional_index(with_fractional_index); - } - - /// Set the jitter of the tree position(Fractional Index). - /// - /// The jitter is used to avoid conflicts when multiple users are creating the node at the same position. - /// value 0 is default, which means no jitter, any value larger than 0 will enable jitter. - /// Generally speaking, jitter will affect the growth rate of document size. - #[inline] - pub fn set_fractional_index_jitter(&self, jitter: u8) { - self.doc.set_fractional_index_jitter(jitter); - } - /// Set the rich text format configuration of the document. /// /// You need to config it if you use rich text `mark` method. @@ -1422,10 +1407,8 @@ impl LoroTree { /// // create a new child /// let child = tree.create(root).unwrap(); /// ``` - pub fn create>>(&self, parent: T) -> LoroResult { - let parent = parent.into(); - let index = self.children_num(parent).unwrap_or(0); - self.handler.create_at(parent, index) + pub fn create>(&self, parent: T) -> LoroResult { + self.handler.create(parent.into()) } /// Get the root nodes of the forest. @@ -1445,17 +1428,18 @@ impl LoroTree { /// /// let doc = LoroDoc::new(); /// let tree = doc.get_tree("tree"); + /// // enable generate fractional index + /// tree.set_enable_fractional_index(0); /// // create a root /// let root = tree.create(None).unwrap(); /// // create a new child at index 0 /// let child = tree.create_at(root, 0).unwrap(); /// ``` - pub fn create_at>>( - &self, - parent: T, - index: usize, - ) -> LoroResult { - self.handler.create_at(parent, index) + pub fn create_at>(&self, parent: T, index: usize) -> LoroResult { + if !self.handler.is_fractional_index_enabled() { + return Err(LoroTreeError::FractionalIndexNotEnabled.into()); + } + self.handler.create_at(parent.into(), index) } /// Move the `target` node to be a child of the `parent` node. @@ -1474,10 +1458,8 @@ impl LoroTree { /// // move `root2` to be a child of `root`. /// tree.mov(root2, root).unwrap(); /// ``` - pub fn mov>>(&self, target: TreeID, parent: T) -> LoroResult<()> { - let parent = parent.into(); - let index = self.children_num(parent).unwrap_or(0); - self.handler.move_to(target, parent, index) + pub fn mov>(&self, target: TreeID, parent: T) -> LoroResult<()> { + self.handler.mov(target, parent.into()) } /// Move the `target` node to be a child of the `parent` node at the given index. @@ -1490,19 +1472,23 @@ impl LoroTree { /// /// let doc = LoroDoc::new(); /// let tree = doc.get_tree("tree"); + /// // enable generate fractional index + /// tree.set_enable_fractional_index(0); /// let root = tree.create(None).unwrap(); /// let root2 = tree.create(None).unwrap(); /// // move `root2` to be a child of `root` at index 0. /// tree.mov_to(root2, root, 0).unwrap(); /// ``` - pub fn mov_to>>( + pub fn mov_to>( &self, target: TreeID, parent: T, to: usize, ) -> LoroResult<()> { - let parent = parent.into(); - self.handler.move_to(target, parent, to) + if !self.handler.is_fractional_index_enabled() { + return Err(LoroTreeError::FractionalIndexNotEnabled.into()); + } + self.handler.move_to(target, parent.into(), to) } /// Move the `target` node to be a child after the `after` node with the same parent. @@ -1514,12 +1500,17 @@ impl LoroTree { /// /// let doc = LoroDoc::new(); /// let tree = doc.get_tree("tree"); + /// // enable generate fractional index + /// tree.set_enable_fractional_index(0); /// let root = tree.create(None).unwrap(); /// let root2 = tree.create(None).unwrap(); /// // move `root` to be a child after `root2`. /// tree.mov_after(root, root2).unwrap(); /// ``` pub fn mov_after(&self, target: TreeID, after: TreeID) -> LoroResult<()> { + if !self.handler.is_fractional_index_enabled() { + return Err(LoroTreeError::FractionalIndexNotEnabled.into()); + } self.handler.mov_after(target, after) } @@ -1532,12 +1523,17 @@ impl LoroTree { /// /// let doc = LoroDoc::new(); /// let tree = doc.get_tree("tree"); + /// // enable generate fractional index + /// tree.set_enable_fractional_index(0); /// let root = tree.create(None).unwrap(); /// let root2 = tree.create(None).unwrap(); /// // move `root` to be a child before `root2`. /// tree.mov_before(root, root2).unwrap(); /// ``` pub fn mov_before(&self, target: TreeID, before: TreeID) -> LoroResult<()> { + if !self.handler.is_fractional_index_enabled() { + return Err(LoroTreeError::FractionalIndexNotEnabled.into()); + } self.handler.mov_before(target, before) } @@ -1582,7 +1578,7 @@ impl LoroTree { /// /// - If the target node does not exist, return `None`. /// - If the target node is a root node, return `Some(None)`. - pub fn parent(&self, target: TreeID) -> Option> { + pub fn parent(&self, target: TreeID) -> Option { self.handler.get_node_parent(&target) } @@ -1599,13 +1595,14 @@ impl LoroTree { /// Return all children of the target node. /// /// If the parent node does not exist, return `None`. - pub fn children(&self, parent: Option) -> Option> { - self.handler.children(parent) + pub fn children>(&self, parent: T) -> Option> { + self.handler.children(&parent.into()) } /// Return the number of children of the target node. - pub fn children_num(&self, parent: Option) -> Option { - self.handler.children_num(parent) + pub fn children_num>(&self, parent: T) -> Option { + let parent: TreeParentId = parent.into(); + self.handler.children_num(&parent) } /// Return container id of the tree. @@ -1639,6 +1636,30 @@ impl LoroTree { pub fn __internal__next_tree_id(&self) -> TreeID { self.handler.__internal__next_tree_id() } + + /// Whether the fractional index is enabled. + pub fn is_fractional_index_enabled(&self) -> bool { + self.handler.is_fractional_index_enabled() + } + + /// Set whether to generate fractional index for Tree Position. + /// + /// The jitter is used to avoid conflicts when multiple users are creating the node at the same position. + /// value 0 is default, which means no jitter, any value larger than 0 will enable jitter. + /// + /// Generally speaking, jitter will affect the growth rate of document size. + /// [Read more about it](https://www.loro.dev/blog/movable-tree#implementation-and-encoding-size) + #[inline] + pub fn set_enable_fractional_index(&self, jitter: u8) { + self.handler.set_enable_fractional_index(jitter); + } + + /// Disable the fractional index generation for Tree Position when + /// you don't need the Tree's siblings to be sorted. The fractional index will be always default. + #[inline] + pub fn set_disable_fractional_index(&self) { + self.handler.set_disable_fractional_index(); + } } impl Default for LoroTree { diff --git a/loro-js/src/index.ts b/loro-js/src/index.ts index 61dd88d0..55a1b09c 100644 --- a/loro-js/src/index.ts +++ b/loro-js/src/index.ts @@ -93,15 +93,17 @@ export type TreeDiffItem = action: "create"; parent: TreeID | undefined; index: number; - position: string; + fractional_index: string; } - | { target: TreeID; action: "delete" } + | { target: TreeID; action: "delete"; old_parent: TreeID | undefined; old_index: number } | { target: TreeID; action: "move"; parent: TreeID | undefined; index: number; - position: string; + fractional_index: string; + old_parent: TreeID | undefined; + old_index: number; }; export type TreeDiff = { diff --git a/loro-js/tests/tree.test.ts b/loro-js/tests/tree.test.ts index bd10a30e..7a930af3 100644 --- a/loro-js/tests/tree.test.ts +++ b/loro-js/tests/tree.test.ts @@ -1,5 +1,5 @@ import { assert, describe, expect, it} from "vitest"; -import { LoroDoc, LoroTree, LoroTreeNode } from "../src"; +import { LoroDoc, LoroTree, LoroTreeNode, TreeDiff } from "../src"; function assertEquals(a: any, b: any) { expect(a).toStrictEqual(b); @@ -8,6 +8,7 @@ function assertEquals(a: any, b: any) { describe("loro tree", () => { const loro = new LoroDoc(); const tree = loro.getTree("root"); + tree.setEnableFractionalIndex(0); it("create", () => { const root = tree.createNode(); @@ -121,6 +122,7 @@ describe("loro tree", () => { describe("loro tree node", ()=>{ const loro = new LoroDoc(); const tree = loro.getTree("root"); + tree.setEnableFractionalIndex(0); it("create", () => { const root = tree.createNode(); @@ -180,6 +182,26 @@ describe("loro tree node", ()=>{ assertEquals(child.index(), 1); assertEquals(child2.index(), 0); }); + + it("old parent", () => { + const root = tree.createNode(); + const child = root.createNode(); + const child2 = root.createNode(); + loro.commit(); + const subID = tree.subscribe((e)=>{ + if(e.events[0].diff.type == "tree"){ + const diff = e.events[0].diff as TreeDiff; + if (diff.diff[0].action == "move"){ + assertEquals(diff.diff[0].old_parent, root.id); + assertEquals(diff.diff[0].old_index, 1); + } + } + }); + child2.move(child); + loro.commit(); + tree.unsubscribe(subID); + assertEquals(child2.parent()!.id, child.id); + }); }); function one_ms(): Promise {