diff --git a/.vscode/settings.json b/.vscode/settings.json index 7c58563b..76d0b720 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,6 +22,7 @@ "opset", "peekable", "Peritext", + "reparent", "RUSTFLAGS", "smstring", "thiserror", @@ -64,5 +65,8 @@ "cortex-debug.variableUseNaturalFormat": true, "[markdown]": { "editor.defaultFormatter": "darkriszty.markdown-table-prettify" + }, + "[typescript]": { + "editor.defaultFormatter": "denoland.vscode-deno" } } diff --git a/crates/loro-common/src/lib.rs b/crates/loro-common/src/lib.rs index b1715824..785a70ac 100644 --- a/crates/loro-common/src/lib.rs +++ b/crates/loro-common/src/lib.rs @@ -241,25 +241,13 @@ mod container { } } -/// In movable tree, we use a specific [`TreeID`] to represent the root of **ALL** non-existent tree nodes. -/// -/// When we create some tree node and then we checkout the previous vision, we need to delete it from the state. -/// If the parent of node is [`UNEXIST_TREE_ROOT`], we could infer this node is first created and delete it from the state directly, -/// instead of moving it to the [`DELETED_TREE_ROOT`]. -/// -/// This root only can be old parent of node. -pub const UNEXIST_TREE_ROOT: Option = Some(TreeID { - peer: PeerID::MAX, - counter: Counter::MAX - 1, -}); - /// In movable tree, we use a specific [`TreeID`] to represent the root of **ALL** deleted tree node. /// /// Deletion operation is equivalent to move target tree node to [`DELETED_TREE_ROOT`]. -pub const DELETED_TREE_ROOT: Option = Some(TreeID { +pub const DELETED_TREE_ROOT: TreeID = TreeID { peer: PeerID::MAX, counter: Counter::MAX, -}); +}; /// Each node of movable tree has a unique [`TreeID`] generated by Loro. /// @@ -283,22 +271,13 @@ impl TreeID { } /// return [`DELETED_TREE_ROOT`] - pub const fn delete_root() -> Option { + pub const fn delete_root() -> Self { DELETED_TREE_ROOT } /// return `true` if the `TreeID` is deleted root - pub fn is_deleted_root(target: Option) -> bool { - target == DELETED_TREE_ROOT - } - - pub const fn unexist_root() -> Option { - UNEXIST_TREE_ROOT - } - - /// return `true` if the `TreeID` is non-existent root - pub fn is_unexist_root(target: Option) -> bool { - target == UNEXIST_TREE_ROOT + pub fn is_deleted_root(target: &TreeID) -> bool { + target == &DELETED_TREE_ROOT } pub fn from_id(id: ID) -> Self { diff --git a/crates/loro-internal/src/container/tree/tree_op.rs b/crates/loro-internal/src/container/tree/tree_op.rs index 8fcaad7b..ec19d2cf 100644 --- a/crates/loro-internal/src/container/tree/tree_op.rs +++ b/crates/loro-internal/src/container/tree/tree_op.rs @@ -2,6 +2,8 @@ use loro_common::TreeID; use rle::{HasLength, Mergable, Sliceable}; use serde::{Deserialize, Serialize}; +use crate::state::TreeParentId; + /// The operation of movable tree. /// /// In the movable tree, there are three actions: @@ -15,6 +17,22 @@ pub struct TreeOp { pub(crate) parent: Option, } +impl TreeOp { + // TODO: use `TreeParentId` instead of `Option` + pub(crate) fn parent_id(&self) -> TreeParentId { + match self.parent { + Some(parent) => { + if TreeID::is_deleted_root(&parent) { + TreeParentId::Deleted + } else { + TreeParentId::Node(parent) + } + } + None => TreeParentId::None, + } + } +} + impl HasLength for TreeOp { fn content_len(&self) -> usize { 1 diff --git a/crates/loro-internal/src/delta/tree.rs b/crates/loro-internal/src/delta/tree.rs index c3c23534..03a34167 100644 --- a/crates/loro-internal/src/delta/tree.rs +++ b/crates/loro-internal/src/delta/tree.rs @@ -6,7 +6,8 @@ use std::{ use fxhash::{FxHashMap, FxHashSet}; use loro_common::{ContainerType, LoroValue, TreeID, ID}; use serde::Serialize; -use smallvec::{smallvec, SmallVec}; + +use crate::state::TreeParentId; #[derive(Debug, Clone, Default, Serialize)] pub struct TreeDiff { @@ -21,51 +22,28 @@ pub struct TreeDiffItem { #[derive(Debug, Clone, Copy, Serialize)] pub enum TreeExternalDiff { - Create, - Move(Option), + Create(TreeParentId), + Move(TreeParentId), Delete, } impl TreeDiffItem { - pub(crate) fn from_delta_item(item: TreeDeltaItem) -> SmallVec<[TreeDiffItem; 2]> { + pub(crate) fn from_delta_item(item: TreeDeltaItem) -> Option { let target = item.target; match item.action { - TreeInternalDiff::Create | TreeInternalDiff::Restore => { - smallvec![TreeDiffItem { - target, - action: TreeExternalDiff::Create - }] - } - TreeInternalDiff::AsRoot => { - smallvec![TreeDiffItem { - target, - action: TreeExternalDiff::Move(None) - }] - } - TreeInternalDiff::Move(p) => { - smallvec![TreeDiffItem { - target, - action: TreeExternalDiff::Move(Some(p)) - }] - } - TreeInternalDiff::CreateMove(p) | TreeInternalDiff::RestoreMove(p) => { - smallvec![ - TreeDiffItem { - target, - action: TreeExternalDiff::Create - }, - TreeDiffItem { - target, - action: TreeExternalDiff::Move(Some(p)) - } - ] - } - TreeInternalDiff::Delete | TreeInternalDiff::UnCreate => { - smallvec![TreeDiffItem { - target, - action: TreeExternalDiff::Delete - }] - } + TreeInternalDiff::Create(p) => Some(TreeDiffItem { + target, + action: TreeExternalDiff::Create(p), + }), + TreeInternalDiff::Move(p) => Some(TreeDiffItem { + target, + action: TreeExternalDiff::Move(p), + }), + TreeInternalDiff::Delete(_) | TreeInternalDiff::UnCreate => Some(TreeDiffItem { + target, + action: TreeExternalDiff::Delete, + }), + TreeInternalDiff::MoveInDelete(_) => None, } } } @@ -82,13 +60,13 @@ impl TreeDiff { } /// Representation of differences in movable tree. It's an ordered list of [`TreeDiff`]. -#[derive(Debug, Clone, Default, Serialize)] +#[derive(Debug, Clone, Default)] pub struct TreeDelta { pub(crate) diff: Vec, } /// The semantic action in movable tree. -#[derive(Debug, Clone, Copy, Serialize)] +#[derive(Debug, Clone, Copy)] pub struct TreeDeltaItem { pub target: TreeID, pub action: TreeInternalDiff, @@ -96,62 +74,47 @@ pub struct TreeDeltaItem { } /// The action of [`TreeDiff`]. It's the same as [`crate::container::tree::tree_op::TreeOp`], but semantic. -#[derive(Debug, Clone, Copy, Serialize)] +#[derive(Debug, Clone, Copy)] pub enum TreeInternalDiff { /// First create the node, have not seen it before - Create, - /// Recreate the node, the node has been deleted before - Restore, - /// Same as move to `None` and the node exists - AsRoot, - /// Move the node to the parent, the node exists - Move(TreeID), - /// First create the node and move it to the parent - CreateMove(TreeID), - /// Recreate the node, and move it to the parent - RestoreMove(TreeID), - /// Delete the node - Delete, + Create(TreeParentId), /// For retreating, if the node is only created, not move it to `DELETED_ROOT` but delete it directly UnCreate, + /// Move the node to the parent, the node exists + Move(TreeParentId), + /// move under a parent that is deleted + Delete(TreeParentId), + /// old parent is deleted, new parent is deleted too + MoveInDelete(TreeParentId), } impl TreeDeltaItem { + /// * `is_new_parent_deleted` and `is_old_parent_deleted`: we need to infer whether it's a `creation`. + /// It's a creation if the old_parent is deleted but the new parent isn't. + /// If it is a creation, we need to emit the `Create` event so that downstream event handler can + /// handle the new containers easier. pub(crate) fn new( target: TreeID, - parent: Option, - old_parent: Option, + parent: TreeParentId, + old_parent: TreeParentId, op_id: ID, - is_parent_deleted: bool, + is_new_parent_deleted: bool, is_old_parent_deleted: bool, ) -> Self { - let action = match (parent, old_parent) { - (Some(p), _) => { - if is_parent_deleted { - TreeInternalDiff::Delete - } else if TreeID::is_unexist_root(parent) { - TreeInternalDiff::UnCreate - } else if TreeID::is_unexist_root(old_parent) { - TreeInternalDiff::CreateMove(p) - } else if is_old_parent_deleted { - TreeInternalDiff::RestoreMove(p) - } else { - TreeInternalDiff::Move(p) - } - } - (None, Some(_)) => { - if TreeID::is_unexist_root(old_parent) { - TreeInternalDiff::Create - } else if is_old_parent_deleted { - TreeInternalDiff::Restore - } else { - TreeInternalDiff::AsRoot - } - } - (None, None) => { - unreachable!() + let action = if matches!(parent, TreeParentId::Unexist) { + TreeInternalDiff::UnCreate + } else { + match ( + is_new_parent_deleted, + is_old_parent_deleted || old_parent == TreeParentId::Unexist, + ) { + (true, true) => TreeInternalDiff::MoveInDelete(parent), + (true, false) => TreeInternalDiff::Delete(parent), + (false, true) => TreeInternalDiff::Create(parent), + (false, false) => TreeInternalDiff::Move(parent), } }; + TreeDeltaItem { target, action, @@ -182,9 +145,12 @@ impl<'a> TreeValue<'a> { for d in diff.diff.iter() { let target = d.target; match d.action { - TreeExternalDiff::Create => self.create_target(target), + TreeExternalDiff::Create(parent) => { + self.create_target(target); + self.mov(target, parent.as_node().copied()); + } TreeExternalDiff::Delete => self.delete_target(target), - TreeExternalDiff::Move(parent) => self.mov(target, parent), + TreeExternalDiff::Move(parent) => self.mov(target, parent.as_node().copied()), } } } diff --git a/crates/loro-internal/src/diff_calc/tree.rs b/crates/loro-internal/src/diff_calc/tree.rs index c8a62a15..103d9776 100644 --- a/crates/loro-internal/src/diff_calc/tree.rs +++ b/crates/loro-internal/src/diff_calc/tree.rs @@ -1,6 +1,6 @@ use std::collections::BTreeSet; -use fxhash::{FxHashMap, FxHashSet}; +use fxhash::FxHashMap; use itertools::Itertools; use loro_common::{ContainerID, HasId, IdSpan, Lamport, TreeID, ID}; @@ -9,6 +9,7 @@ use crate::{ dag::DagUtils, delta::{TreeDelta, TreeDeltaItem, TreeInternalDiff}, event::InternalDiff, + state::TreeParentId, version::Frontiers, OpLog, VersionVector, }; @@ -44,13 +45,7 @@ impl DiffCalculatorTrait for TreeDiffCalculator { diff.diff.iter().for_each(|d| { // the metadata could be modified before, so (re)create a node need emit the map container diffs // `Create` here is because maybe in a diff calc uncreate and then create back - if matches!( - d.action, - TreeInternalDiff::Restore - | TreeInternalDiff::RestoreMove(_) - | TreeInternalDiff::Create - | TreeInternalDiff::CreateMove(_) - ) { + if matches!(d.action, TreeInternalDiff::Create(_)) { on_new_container(&d.target.associated_meta_container()) } }); @@ -129,7 +124,7 @@ impl TreeDiffCalculator { for (lamport, op) in forward_ops { let op = MoveLamportAndID { target: op.value.target, - parent: op.value.parent, + parent: op.value.parent_id(), id: op.id_start(), lamport, effected: false, @@ -188,13 +183,11 @@ impl TreeDiffCalculator { op.id.counter, op.id.counter + 1, )); - let (old_parent, last_effective_move_op_id) = tree_cache.get_parent(op.target); + let (old_parent, last_effective_move_op_id) = tree_cache.get_parent_with_id(op.target); if op.effected { // we need to know whether old_parent is deleted - let is_parent_deleted = - op.parent.is_some() && tree_cache.is_deleted(*op.parent.as_ref().unwrap()); - let is_old_parent_deleted = - old_parent.is_some() && tree_cache.is_deleted(*old_parent.as_ref().unwrap()); + let is_parent_deleted = tree_cache.is_parent_deleted(op.parent); + let is_old_parent_deleted = tree_cache.is_parent_deleted(old_parent); let this_diff = TreeDeltaItem::new( op.target, old_parent, @@ -204,17 +197,14 @@ impl TreeDiffCalculator { is_parent_deleted, ); diffs.push(this_diff); - if matches!( - this_diff.action, - TreeInternalDiff::Restore | TreeInternalDiff::RestoreMove(_) - ) { + if matches!(this_diff.action, TreeInternalDiff::Create(_)) { let mut s = vec![op.target]; while let Some(t) = s.pop() { - let children = tree_cache.get_children(t); + let children = tree_cache.get_children_with_id(TreeParentId::Node(t)); children.iter().for_each(|c| { diffs.push(TreeDeltaItem { target: c.0, - action: TreeInternalDiff::CreateMove(t), + action: TreeInternalDiff::Create(TreeParentId::Node(t)), last_effective_move_op_id: c.1, }) }); @@ -239,16 +229,14 @@ impl TreeDiffCalculator { { let op = MoveLamportAndID { target: op.value.target, - parent: op.value.parent, + parent: op.value.parent_id(), id: op.id_start(), lamport: *lamport, effected: false, }; - let (old_parent, _id) = tree_cache.get_parent(op.target); - let is_parent_deleted = - op.parent.is_some() && tree_cache.is_deleted(*op.parent.as_ref().unwrap()); - let is_old_parent_deleted = old_parent.is_some() - && tree_cache.is_deleted(*old_parent.as_ref().unwrap()); + let (old_parent, _id) = tree_cache.get_parent_with_id(op.target); + let is_parent_deleted = tree_cache.is_parent_deleted(op.parent); + let is_old_parent_deleted = tree_cache.is_parent_deleted(old_parent); let effected = tree_cache.apply(op); if effected { let this_diff = TreeDeltaItem::new( @@ -260,18 +248,16 @@ impl TreeDiffCalculator { is_old_parent_deleted, ); diffs.push(this_diff); - if matches!( - this_diff.action, - TreeInternalDiff::Restore | TreeInternalDiff::RestoreMove(_) - ) { + if matches!(this_diff.action, TreeInternalDiff::Create(_)) { // TODO: per let mut s = vec![op.target]; while let Some(t) = s.pop() { - let children = tree_cache.get_children(t); + let children = + tree_cache.get_children_with_id(TreeParentId::Node(t)); children.iter().for_each(|c| { diffs.push(TreeDeltaItem { target: c.0, - action: TreeInternalDiff::CreateMove(t), + action: TreeInternalDiff::Create(TreeParentId::Node(t)), last_effective_move_op_id: c.1, }) }); @@ -302,70 +288,32 @@ impl TreeDiffCalculator { } } -pub(crate) trait TreeDeletedSetTrait { - fn deleted(&self) -> &FxHashSet; - fn deleted_mut(&mut self) -> &mut FxHashSet; - fn get_children(&self, target: TreeID) -> Vec<(TreeID, ID)>; - fn get_children_recursively(&self, target: TreeID) -> Vec<(TreeID, ID)> { - let mut ans = vec![]; - let mut s = vec![target]; - while let Some(t) = s.pop() { - let children = self.get_children(t); - ans.extend(children.clone()); - s.extend(children.iter().map(|x| x.0)); - } - ans - } - fn is_deleted(&self, target: &TreeID) -> bool { - self.deleted().contains(target) || TreeID::is_deleted_root(Some(*target)) - } - fn update_deleted_cache( - &mut self, - target: TreeID, - parent: Option, - old_parent: Option, - ) { - if parent.is_some() && self.is_deleted(&parent.unwrap()) { - self.update_deleted_cache_inner(target, true); - } else if let Some(old_parent) = old_parent { - if self.is_deleted(&old_parent) { - self.update_deleted_cache_inner(target, false); - } - } - } - fn update_deleted_cache_inner(&mut self, target: TreeID, set_children_deleted: bool) { - if set_children_deleted { - self.deleted_mut().insert(target); - } else { - self.deleted_mut().remove(&target); - } - let mut s = self.get_children(target); - while let Some((child, _)) = s.pop() { - if child == target { - continue; - } - if set_children_deleted { - self.deleted_mut().insert(child); - } else { - self.deleted_mut().remove(&child); - } - s.extend(self.get_children(child)) - } - } -} - /// All information of an operation for diff calculating of movable tree. -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct MoveLamportAndID { pub(crate) lamport: Lamport, pub(crate) id: ID, pub(crate) target: TreeID, - pub(crate) parent: Option, + pub(crate) parent: TreeParentId, /// Whether this action is applied in the current version. /// If this action will cause a circular reference, then this action will not be applied. pub(crate) effected: bool, } +impl PartialOrd for MoveLamportAndID { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for MoveLamportAndID { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.lamport + .cmp(&other.lamport) + .then_with(|| self.id.cmp(&other.id)) + } +} + impl core::hash::Hash for MoveLamportAndID { fn hash(&self, ra_expand_state: &mut H) { let MoveLamportAndID { lamport, id, .. } = self; @@ -383,29 +331,51 @@ pub(crate) struct TreeCacheForDiff { } impl TreeCacheForDiff { - fn is_ancestor_of(&self, maybe_ancestor: TreeID, mut node_id: TreeID) -> bool { - if maybe_ancestor == node_id { - return true; + fn is_ancestor_of(&self, maybe_ancestor: &TreeID, node_id: &TreeParentId) -> bool { + if !self.tree.contains_key(maybe_ancestor) { + return false; } - - loop { - let (parent, _id) = self.get_parent(node_id); - match parent { - Some(parent_id) if parent_id == maybe_ancestor => return true, - Some(parent_id) if parent_id == node_id => panic!("loop detected"), - Some(parent_id) => { - node_id = parent_id; - } - None => return false, + if let TreeParentId::Node(id) = node_id { + if id == maybe_ancestor { + return true; } } - } - /// get the parent of the first effected op and its id - fn get_parent(&self, tree_id: TreeID) -> (Option, ID) { - if TreeID::is_deleted_root(Some(tree_id)) { - return (None, ID::NONE_ID); + match node_id { + TreeParentId::Node(id) => { + let (parent, _) = &self.get_parent_with_id(*id); + if parent == node_id { + panic!("is_ancestor_of loop") + } + self.is_ancestor_of(maybe_ancestor, parent) + } + TreeParentId::Deleted | TreeParentId::None => false, + TreeParentId::Unexist => unreachable!(), } - let mut ans = (TreeID::unexist_root(), ID::NONE_ID); + } + + fn apply(&mut self, mut node: MoveLamportAndID) -> bool { + let mut effected = true; + if self.is_ancestor_of(&node.target, &node.parent) { + effected = false; + } + node.effected = effected; + self.tree.entry(node.target).or_default().insert(node); + self.current_vv.set_last(node.id); + effected + } + + fn is_parent_deleted(&self, parent: TreeParentId) -> bool { + match parent { + TreeParentId::Deleted => true, + TreeParentId::Node(id) => self.is_parent_deleted(self.get_parent_with_id(id).0), + TreeParentId::None => false, + TreeParentId::Unexist => false, + } + } + + /// get the parent of the first effected op and its id + fn get_parent_with_id(&self, tree_id: TreeID) -> (TreeParentId, ID) { + let mut ans = (TreeParentId::Unexist, ID::NONE_ID); if let Some(cache) = self.tree.get(&tree_id) { for op in cache.iter().rev() { if op.effected { @@ -417,36 +387,9 @@ impl TreeCacheForDiff { ans } - fn apply(&mut self, mut node: MoveLamportAndID) -> bool { - let mut effected = true; - if node.parent.is_some() && self.is_ancestor_of(node.target, node.parent.unwrap()) { - effected = false; - } - node.effected = effected; - self.tree.entry(node.target).or_default().insert(node); - self.current_vv.set_last(node.id); - effected - } - - fn is_deleted(&self, mut target: TreeID) -> bool { - if TreeID::is_deleted_root(Some(target)) { - return true; - } - if TreeID::is_unexist_root(Some(target)) { - return false; - } - while let (Some(parent), _) = self.get_parent(target) { - if TreeID::is_deleted_root(Some(parent)) { - return true; - } - target = parent; - } - false - } - - /// get the parent of the first effected op + /// get the parent of the last effected op fn get_last_effective_move(&self, tree_id: TreeID) -> Option<&MoveLamportAndID> { - if TreeID::is_deleted_root(Some(tree_id)) { + if TreeID::is_deleted_root(&tree_id) { return None; } @@ -463,17 +406,14 @@ impl TreeCacheForDiff { ans } - fn get_children(&self, target: TreeID) -> Vec<(TreeID, ID)> { + fn get_children_with_id(&self, parent: TreeParentId) -> Vec<(TreeID, ID)> { let mut ans = vec![]; for (tree_id, _) in self.tree.iter() { - if tree_id == &target { - continue; - } let Some(op) = self.get_last_effective_move(*tree_id) else { continue; }; - if op.parent == Some(target) { + if op.parent == parent { ans.push((*tree_id, op.id)); } } diff --git a/crates/loro-internal/src/encoding/encode_reordered.rs b/crates/loro-internal/src/encoding/encode_reordered.rs index bc0ec012..f58a49e6 100644 --- a/crates/loro-internal/src/encoding/encode_reordered.rs +++ b/crates/loro-internal/src/encoding/encode_reordered.rs @@ -2041,7 +2041,7 @@ mod arena { use super::{encode::ValueRegister, PeerIdx, MAX_DECODED_SIZE}; - pub fn encode_arena( + pub(super) fn encode_arena( peer_ids_arena: Vec, containers: ContainerArena, keys: Vec, @@ -2065,9 +2065,9 @@ mod arena { } pub struct DecodedArenas<'a> { - pub peer_ids: PeerIdArena, - pub containers: ContainerArena, - pub keys: KeyArena, + pub(super) peer_ids: PeerIdArena, + pub(super) containers: ContainerArena, + pub(super) keys: KeyArena, pub deps: Box + 'a>, pub state_blob_arena: &'a [u8], } diff --git a/crates/loro-internal/src/event.rs b/crates/loro-internal/src/event.rs index 8b7bfe84..2f9d19c0 100644 --- a/crates/loro-internal/src/event.rs +++ b/crates/loro-internal/src/event.rs @@ -133,7 +133,7 @@ impl DiffVariant { } #[non_exhaustive] -#[derive(Clone, Debug, EnumAsInner, Serialize)] +#[derive(Clone, Debug, EnumAsInner)] pub(crate) enum InternalDiff { ListRaw(Delta), /// This always uses entity indexes. diff --git a/crates/loro-internal/src/handler.rs b/crates/loro-internal/src/handler.rs index 301b39cc..b6af4835 100644 --- a/crates/loro-internal/src/handler.rs +++ b/crates/loro-internal/src/handler.rs @@ -9,7 +9,7 @@ use crate::{ }, delta::{DeltaItem, StyleMeta, TreeDiffItem, TreeExternalDiff}, op::ListSlice, - state::RichtextState, + state::{RichtextState, TreeParentId}, txn::EventHint, utils::{string_slice::StringSlice, utf16::count_utf16_len}, }; @@ -20,7 +20,6 @@ use loro_common::{ TreeID, }; use serde::{Deserialize, Serialize}; -use smallvec::smallvec; use std::{ borrow::Cow, ops::Deref, @@ -1227,12 +1226,12 @@ impl TreeHandler { self.container_idx, crate::op::RawOpContent::Tree(TreeOp { target, - parent: TreeID::delete_root(), + parent: Some(TreeID::delete_root()), }), - EventHint::Tree(smallvec![TreeDiffItem { + EventHint::Tree(TreeDiffItem { target, action: TreeExternalDiff::Delete, - }]), + }), &self.state, ) } @@ -1246,18 +1245,12 @@ impl TreeHandler { txn: &mut Transaction, parent: T, ) -> LoroResult { - let parent = parent.into(); + let parent: Option = parent.into(); let tree_id = TreeID::from_id(txn.next_id()); - let mut event_hint = smallvec![TreeDiffItem { + let event_hint = TreeDiffItem { target: tree_id, - action: TreeExternalDiff::Create, - },]; - if parent.is_some() { - event_hint.push(TreeDiffItem { - target: tree_id, - action: TreeExternalDiff::Move(parent), - }); - } + action: TreeExternalDiff::Create(TreeParentId::from_tree_id(parent)), + }; txn.apply_local_op( self.container_idx, crate::op::RawOpContent::Tree(TreeOp { @@ -1284,10 +1277,10 @@ impl TreeHandler { txn.apply_local_op( self.container_idx, crate::op::RawOpContent::Tree(TreeOp { target, parent }), - EventHint::Tree(smallvec![TreeDiffItem { + EventHint::Tree(TreeDiffItem { target, - action: TreeExternalDiff::Move(parent), - }]), + action: TreeExternalDiff::Move(TreeParentId::from_tree_id(parent)), + }), &self.state, ) } @@ -1309,6 +1302,7 @@ impl TreeHandler { Ok(map) } + /// Get the parent of the node, if the node is deleted or does not exist, return None pub fn parent(&self, target: TreeID) -> Option> { self.state .upgrade() @@ -1317,7 +1311,26 @@ impl TreeHandler { .unwrap() .with_state(self.container_idx, |state| { let a = state.as_tree_state().unwrap(); - a.parent(target) + a.parent(target).map(|p| match p { + TreeParentId::None => None, + TreeParentId::Node(parent_id) => Some(parent_id), + _ => unreachable!(), + }) + }) + } + + pub fn children(&self, target: TreeID) -> Vec { + self.state + .upgrade() + .unwrap() + .lock() + .unwrap() + .with_state(self.container_idx, |state| { + let a = state.as_tree_state().unwrap(); + a.as_ref() + .get_children(&TreeParentId::Node(target)) + .into_iter() + .collect() }) } diff --git a/crates/loro-internal/src/oplog/dag.rs b/crates/loro-internal/src/oplog/dag.rs index 5d83deed..cd19a4ce 100644 --- a/crates/loro-internal/src/oplog/dag.rs +++ b/crates/loro-internal/src/oplog/dag.rs @@ -1,5 +1,5 @@ use std::cmp::Ordering; -use std::fmt::{Display, Write}; +use std::fmt::Display; use crate::change::Lamport; use crate::dag::{Dag, DagNode}; diff --git a/crates/loro-internal/src/state.rs b/crates/loro-internal/src/state.rs index c0f57b4b..4cfff1a9 100644 --- a/crates/loro-internal/src/state.rs +++ b/crates/loro-internal/src/state.rs @@ -34,7 +34,7 @@ mod tree_state; 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, TreeState}; +pub(crate) use tree_state::{get_meta_value, TreeParentId, TreeState}; use super::{arena::SharedArena, event::InternalDocDiff}; diff --git a/crates/loro-internal/src/state/tree_state.rs b/crates/loro-internal/src/state/tree_state.rs index d772e40f..e9ffcaa6 100644 --- a/crates/loro-internal/src/state/tree_state.rs +++ b/crates/loro-internal/src/state/tree_state.rs @@ -1,4 +1,5 @@ -use fxhash::{FxHashMap, FxHashSet}; +use enum_as_inner::EnumAsInner; +use fxhash::FxHashMap; use itertools::Itertools; use loro_common::{ContainerID, LoroError, LoroResult, LoroTreeError, LoroValue, TreeID, ID}; use rle::HasLength; @@ -8,7 +9,6 @@ use std::sync::{Arc, Mutex, Weak}; use crate::container::idx::ContainerIdx; use crate::delta::{TreeDiff, TreeDiffItem, TreeExternalDiff}; -use crate::diff_calc::tree::TreeDeletedSetTrait; use crate::encoding::{EncodeMode, StateSnapshotDecodeContext, StateSnapshotEncoder}; use crate::event::InternalDiff; use crate::txn::Transaction; @@ -23,6 +23,39 @@ use crate::{ use super::ContainerState; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumAsInner, Serialize)] +pub enum TreeParentId { + Node(TreeID), + Unexist, + Deleted, + /// parent is root + None, +} + +impl TreeParentId { + pub(crate) fn from_tree_id(id: Option) -> Self { + match id { + Some(id) => { + if TreeID::is_deleted_root(&id) { + TreeParentId::Deleted + } else { + TreeParentId::Node(id) + } + } + None => TreeParentId::None, + } + } + + pub(crate) fn to_tree_id(self) -> Option { + match self { + TreeParentId::Node(id) => Some(id), + TreeParentId::Deleted => Some(TreeID::delete_root()), + TreeParentId::None => None, + TreeParentId::Unexist => unreachable!(), + } + } +} + /// The state of movable tree. /// /// using flat representation @@ -30,152 +63,107 @@ use super::ContainerState; pub struct TreeState { idx: ContainerIdx, pub(crate) trees: FxHashMap, - pub(crate) deleted: FxHashSet, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct TreeStateNode { - pub parent: Option, + pub parent: TreeParentId, pub last_move_op: ID, } -impl TreeStateNode { - pub const UNEXIST_ROOT: TreeStateNode = TreeStateNode { - parent: TreeID::unexist_root(), - last_move_op: ID::NONE_ID, - }; -} - -impl Ord for TreeStateNode { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.parent.cmp(&other.parent) - } -} - -impl PartialOrd for TreeStateNode { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - impl TreeState { pub fn new(idx: ContainerIdx) -> Self { - let mut trees = FxHashMap::default(); - trees.insert( - TreeID::delete_root().unwrap(), - TreeStateNode { - parent: None, - last_move_op: ID::NONE_ID, - }, - ); - trees.insert( - TreeID::unexist_root().unwrap(), - TreeStateNode { - parent: None, - last_move_op: ID::NONE_ID, - }, - ); - let mut deleted = FxHashSet::default(); - deleted.insert(TreeID::delete_root().unwrap()); Self { idx, - trees, - deleted, + trees: FxHashMap::default(), } } - pub fn mov(&mut self, target: TreeID, parent: Option, id: ID) -> Result<(), LoroError> { - let Some(parent) = parent else { + pub fn mov(&mut self, target: TreeID, parent: TreeParentId, id: ID) -> Result<(), LoroError> { + if parent.is_none() { // new root node - let old_parent = self - .trees - .insert( - target, - TreeStateNode { - parent: None, - last_move_op: id, - }, - ) - .unwrap_or(TreeStateNode::UNEXIST_ROOT); - self.update_deleted_cache(target, None, old_parent.parent); + self.trees.insert( + target, + TreeStateNode { + parent, + last_move_op: id, + }, + ); return Ok(()); }; - if !self.contains(parent) { - return Err(LoroTreeError::TreeNodeParentNotFound(parent).into()); + if let TreeParentId::Node(parent) = parent { + if !self.trees.contains_key(&parent) { + return Err(LoroTreeError::TreeNodeParentNotFound(parent).into()); + } } if self.is_ancestor_of(&target, &parent) { return Err(LoroTreeError::CyclicMoveError.into()); } - if self - .trees - .get(&target) - .map(|x| x.parent) - .unwrap_or(TreeID::unexist_root()) - == Some(parent) - { - return Ok(()); - } // move or delete or create children node - let old_parent = self - .trees - .insert( - target, - TreeStateNode { - parent: Some(parent), - last_move_op: id, - }, - ) - .unwrap_or(TreeStateNode::UNEXIST_ROOT); - self.update_deleted_cache(target, Some(parent), old_parent.parent); + self.trees.insert( + target, + TreeStateNode { + parent, + last_move_op: id, + }, + ); Ok(()) } #[inline(never)] - fn is_ancestor_of(&self, maybe_ancestor: &TreeID, node_id: &TreeID) -> bool { + fn is_ancestor_of(&self, maybe_ancestor: &TreeID, node_id: &TreeParentId) -> bool { if !self.trees.contains_key(maybe_ancestor) { return false; } - if maybe_ancestor == node_id { - return true; - } - - let mut node_id = node_id; - loop { - let parent = &self.trees.get(node_id).unwrap().parent; - match parent { - Some(parent_id) if parent_id == maybe_ancestor => return true, - Some(parent_id) if parent_id == node_id => panic!("loop detected"), - Some(parent_id) => { - node_id = parent_id; - } - None => return false, + if let TreeParentId::Node(id) = node_id { + if id == maybe_ancestor { + return true; } } + match node_id { + TreeParentId::Node(id) => { + let parent = &self.trees.get(id).unwrap().parent; + if parent == node_id { + panic!("is_ancestor_of loop") + } + self.is_ancestor_of(maybe_ancestor, parent) + } + TreeParentId::Deleted | TreeParentId::None => false, + TreeParentId::Unexist => unreachable!(), + } } pub fn contains(&self, target: TreeID) -> bool { - if TreeID::is_deleted_root(Some(target)) { - return true; - } - !self.is_deleted(&target) + !self.is_node_deleted(&target) } - pub fn parent(&self, target: TreeID) -> Option> { - if self.is_deleted(&target) { + /// Get the parent of the node, if the node is deleted or does not exist, return None + pub fn parent(&self, target: TreeID) -> Option { + if self.is_node_deleted(&target) { None } else { self.trees.get(&target).map(|x| x.parent) } } - fn is_deleted(&self, target: &TreeID) -> bool { - self.deleted.contains(target) + /// If the node is not deleted or does not exist, return false. + /// only the node is deleted and exists, return true + fn is_node_deleted(&self, target: &TreeID) -> bool { + match self.trees.get(target) { + Some(x) => match x.parent { + TreeParentId::Deleted => true, + TreeParentId::None => false, + TreeParentId::Node(p) => self.is_node_deleted(&p), + TreeParentId::Unexist => unreachable!(), + }, + None => false, + } } pub fn nodes(&self) -> Vec { self.trees .keys() - .filter(|&k| !self.is_deleted(k) && !TreeID::is_unexist_root(Some(*k))) + .filter(|&k| !self.is_node_deleted(k)) .copied() .collect::>() } @@ -184,25 +172,20 @@ impl TreeState { pub fn max_counter(&self) -> i32 { self.trees .keys() - .filter(|&k| !self.is_deleted(k) && !TreeID::is_unexist_root(Some(*k))) + .filter(|&k| !self.is_node_deleted(k)) .map(|k| k.counter) .max() .unwrap_or(0) } - fn get_is_deleted_by_query(&self, target: TreeID) -> bool { - match self.trees.get(&target) { - Some(x) => { - if x.parent.is_none() { - false - } else if x.parent == TreeID::delete_root() { - true - } else { - self.get_is_deleted_by_query(x.parent.unwrap()) - } + pub fn get_children(&self, parent: &TreeParentId) -> Vec { + let mut ans = Vec::new(); + for (t, p) in self.trees.iter() { + if &p.parent == parent { + ans.push(*t); } - None => false, } + ans } } @@ -216,7 +199,7 @@ impl ContainerState for TreeState { } fn is_state_empty(&self) -> bool { - self.trees.is_empty() + self.nodes().is_empty() } fn apply_diff_and_convert( @@ -232,32 +215,23 @@ impl ContainerState for TreeState { let target = diff.target; // create associated metadata container let parent = match diff.action { - TreeInternalDiff::Create - | TreeInternalDiff::Restore - | TreeInternalDiff::AsRoot => None, - TreeInternalDiff::Move(parent) - | TreeInternalDiff::CreateMove(parent) - | TreeInternalDiff::RestoreMove(parent) => Some(parent), - TreeInternalDiff::Delete => TreeID::delete_root(), + TreeInternalDiff::Create(p) + | TreeInternalDiff::Move(p) + | TreeInternalDiff::Delete(p) + | TreeInternalDiff::MoveInDelete(p) => p, TreeInternalDiff::UnCreate => { // delete it from state self.trees.remove(&target); continue; } }; - let old = self - .trees - .insert( - target, - TreeStateNode { - parent, - last_move_op: diff.last_effective_move_op_id, - }, - ) - .unwrap_or(TreeStateNode::UNEXIST_ROOT); - if parent != old.parent { - self.update_deleted_cache(target, parent, old.parent); - } + self.trees.insert( + target, + TreeStateNode { + parent, + last_move_op: diff.last_effective_move_op_id, + }, + ); } } let ans = diff @@ -265,7 +239,7 @@ impl ContainerState for TreeState { .unwrap() .diff .into_iter() - .flat_map(TreeDiffItem::from_delta_item) + .filter_map(TreeDiffItem::from_delta_item) .collect_vec(); Diff::Tree(TreeDiff { diff: ans }) } @@ -284,6 +258,17 @@ impl ContainerState for TreeState { match raw_op.content { crate::op::RawOpContent::Tree(tree) => { let TreeOp { target, parent, .. } = tree; + // TODO: use TreeParentId + let parent = match parent { + Some(parent) => { + if TreeID::is_deleted_root(&parent) { + TreeParentId::Deleted + } else { + TreeParentId::Node(parent) + } + } + None => TreeParentId::None, + }; self.mov(target, parent, raw_op.id) } _ => unreachable!(), @@ -301,18 +286,14 @@ impl ContainerState for TreeState { let forest = Forest::from_tree_state(&self.trees); let mut q = VecDeque::from(forest.roots); while let Some(node) = q.pop_front() { - let action = if let Some(parent) = node.parent { - diffs.push(TreeDiffItem { - target: node.id, - action: TreeExternalDiff::Create, - }); - TreeExternalDiff::Move(Some(parent)) + let parent = if let Some(p) = node.parent { + TreeParentId::Node(p) } else { - TreeExternalDiff::Create + TreeParentId::None }; let diff = TreeDiffItem { target: node.id, - action, + action: TreeExternalDiff::Create(parent), }; diffs.push(diff); q.extend(node.children); @@ -325,15 +306,17 @@ impl ContainerState for TreeState { let mut ans: Vec = vec![]; #[cfg(feature = "test_utils")] // The order keep consistent - let iter = self.trees.iter().sorted(); + let iter = self.trees.keys().sorted(); #[cfg(not(feature = "test_utils"))] - let iter = self.trees.iter(); - for (target, node) in iter { - if !self.deleted.contains(target) && !TreeID::is_unexist_root(Some(*target)) { + let iter = self.trees.keys(); + for target in iter { + if !self.is_node_deleted(target) { + let node = self.trees.get(target).unwrap(); let mut t = FxHashMap::default(); t.insert("id".to_string(), target.id().to_string().into()); let p = node .parent + .as_node() .map(|p| p.to_string().into()) .unwrap_or(LoroValue::Null); t.insert("parent".to_string(), p); @@ -383,6 +366,17 @@ impl ContainerState for TreeState { let content = op.op.content.as_tree().unwrap(); let target = content.target; let parent = content.parent; + // TODO: use TreeParentId + let parent = match parent { + Some(parent) => { + if TreeID::is_deleted_root(&parent) { + TreeParentId::Deleted + } else { + TreeParentId::Node(parent) + } + } + None => TreeParentId::None, + }; self.trees.insert( target, TreeStateNode { @@ -391,34 +385,6 @@ impl ContainerState for TreeState { }, ); } - - for t in self.trees.keys() { - if self.get_is_deleted_by_query(*t) { - self.deleted.insert(*t); - } - } - } -} - -impl TreeDeletedSetTrait for TreeState { - fn deleted(&self) -> &FxHashSet { - &self.deleted - } - - fn deleted_mut(&mut self) -> &mut FxHashSet { - &mut self.deleted - } - - fn get_children(&self, target: TreeID) -> Vec<(TreeID, ID)> { - let mut ans = Vec::new(); - for (t, parent) in self.trees.iter() { - if let Some(p) = parent.parent { - if p == target { - ans.push((*t, parent.last_move_op)); - } - } - } - ans } } @@ -427,13 +393,13 @@ impl TreeDeletedSetTrait for TreeState { /// ```json /// { /// "roots": [......], -/// "deleted": [......] +/// // "deleted": [......] /// } /// ``` #[derive(Debug, Default, Serialize, Deserialize)] pub struct Forest { pub roots: Vec, - deleted: Vec, + // deleted: Vec, } /// The node with metadata in hierarchy tree structure. @@ -448,68 +414,59 @@ pub struct TreeNode { impl Forest { pub(crate) fn from_tree_state(state: &FxHashMap) -> Self { let mut forest = Self::default(); - let mut node_to_children = FxHashMap::default(); + let mut parent_id_to_children = FxHashMap::default(); - for (id, parent) in state.iter().sorted() { - if let Some(parent) = &parent.parent { - node_to_children - .entry(*parent) - .or_insert_with(Vec::new) - .push(*id) - } + for id in state.keys().sorted() { + let parent = state.get(id).unwrap(); + parent_id_to_children + .entry(parent.parent) + .or_insert_with(Vec::new) + .push(*id) } - for root in state - .iter() - .filter(|(_, parent)| parent.parent.is_none()) - .map(|(id, _)| *id) - .sorted() - { - if root == TreeID::unexist_root().unwrap() { - continue; - } - let mut stack = vec![( - root, - TreeNode { - id: root, - parent: None, - meta: LoroValue::Container(root.associated_meta_container()), - children: vec![], - }, - )]; - let mut id_to_node = FxHashMap::default(); - while let Some((id, mut node)) = stack.pop() { - if let Some(children) = node_to_children.get(&id) { - let mut children_to_stack = Vec::new(); - for child in children { - if let Some(child_node) = id_to_node.remove(child) { - node.children.push(child_node); - } else { - children_to_stack.push(( - *child, - TreeNode { - id: *child, - parent: Some(id), - meta: LoroValue::Container(child.associated_meta_container()), - children: vec![], - }, - )); + if let Some(roots) = parent_id_to_children.get(&TreeParentId::None) { + for root in roots.iter().copied() { + let mut stack = vec![( + root, + TreeNode { + id: root, + parent: None, + meta: LoroValue::Container(root.associated_meta_container()), + children: vec![], + }, + )]; + let mut id_to_node = FxHashMap::default(); + while let Some((id, mut node)) = stack.pop() { + if let Some(children) = parent_id_to_children.get(&TreeParentId::Node(id)) { + let mut children_to_stack = Vec::new(); + for child in children { + if let Some(child_node) = id_to_node.remove(child) { + node.children.push(child_node); + } else { + children_to_stack.push(( + *child, + TreeNode { + id: *child, + parent: Some(id), + meta: LoroValue::Container( + child.associated_meta_container(), + ), + children: vec![], + }, + )); + } + } + if !children_to_stack.is_empty() { + stack.push((id, node)); + stack.extend(children_to_stack); + } else { + id_to_node.insert(id, node); } - } - if !children_to_stack.is_empty() { - stack.push((id, node)); - stack.extend(children_to_stack); } else { id_to_node.insert(id, node); } - } else { - id_to_node.insert(id, node); } - } - let root_node = id_to_node.remove(&root).unwrap(); - if root_node.id == TreeID::delete_root().unwrap() { - forest.deleted = root_node.children; - } else { + let root_node = id_to_node.remove(&root).unwrap(); forest.roots.push(root_node); } } @@ -554,8 +511,10 @@ mod tests { 0, loro_common::ContainerType::Tree, )); - state.mov(ID1, None, ID::NONE_ID).unwrap(); - state.mov(ID2, Some(ID1), ID::NONE_ID).unwrap(); + state.mov(ID1, TreeParentId::None, ID::NONE_ID).unwrap(); + state + .mov(ID2, TreeParentId::Node(ID1), ID::NONE_ID) + .unwrap(); } #[test] @@ -564,13 +523,15 @@ mod tests { 0, loro_common::ContainerType::Tree, )); - state.mov(ID1, None, ID::NONE_ID).unwrap(); - state.mov(ID2, Some(ID1), ID::NONE_ID).unwrap(); + state.mov(ID1, TreeParentId::None, ID::NONE_ID).unwrap(); + state + .mov(ID2, TreeParentId::Node(ID1), ID::NONE_ID) + .unwrap(); let roots = Forest::from_tree_state(&state.trees); let json = serde_json::to_string(&roots).unwrap(); assert_eq!( json, - r#"{"roots":[{"id":{"peer":0,"counter":0},"meta":{"Container":{"Normal":{"peer":0,"counter":0,"container_type":"Map"}}},"parent":null,"children":[{"id":{"peer":0,"counter":1},"meta":{"Container":{"Normal":{"peer":0,"counter":1,"container_type":"Map"}}},"parent":{"peer":0,"counter":0},"children":[]}]}],"deleted":[]}"# + r#"{"roots":[{"id":{"peer":0,"counter":0},"meta":{"Container":{"Normal":{"peer":0,"counter":0,"container_type":"Map"}}},"parent":null,"children":[{"id":{"peer":0,"counter":1},"meta":{"Container":{"Normal":{"peer":0,"counter":1,"container_type":"Map"}}},"parent":{"peer":0,"counter":0},"children":[]}]}]}"# ) } @@ -580,16 +541,22 @@ mod tests { 0, loro_common::ContainerType::Tree, )); - state.mov(ID1, None, ID::NONE_ID).unwrap(); - state.mov(ID2, Some(ID1), ID::NONE_ID).unwrap(); - state.mov(ID3, Some(ID2), ID::NONE_ID).unwrap(); - state.mov(ID4, Some(ID1), ID::NONE_ID).unwrap(); - state.mov(ID2, TreeID::delete_root(), ID::NONE_ID).unwrap(); + state.mov(ID1, TreeParentId::None, ID::NONE_ID).unwrap(); + state + .mov(ID2, TreeParentId::Node(ID1), ID::NONE_ID) + .unwrap(); + state + .mov(ID3, TreeParentId::Node(ID2), ID::NONE_ID) + .unwrap(); + state + .mov(ID4, TreeParentId::Node(ID1), ID::NONE_ID) + .unwrap(); + state.mov(ID2, TreeParentId::Deleted, ID::NONE_ID).unwrap(); let roots = Forest::from_tree_state(&state.trees); let json = serde_json::to_string(&roots).unwrap(); assert_eq!( json, - r#"{"roots":[{"id":{"peer":0,"counter":0},"meta":{"Container":{"Normal":{"peer":0,"counter":0,"container_type":"Map"}}},"parent":null,"children":[{"id":{"peer":0,"counter":3},"meta":{"Container":{"Normal":{"peer":0,"counter":3,"container_type":"Map"}}},"parent":{"peer":0,"counter":0},"children":[]}]}],"deleted":[{"id":{"peer":0,"counter":1},"meta":{"Container":{"Normal":{"peer":0,"counter":1,"container_type":"Map"}}},"parent":{"peer":18446744073709551615,"counter":2147483647},"children":[{"id":{"peer":0,"counter":2},"meta":{"Container":{"Normal":{"peer":0,"counter":2,"container_type":"Map"}}},"parent":{"peer":0,"counter":1},"children":[]}]}]}"# + r#"{"roots":[{"id":{"peer":0,"counter":0},"meta":{"Container":{"Normal":{"peer":0,"counter":0,"container_type":"Map"}}},"parent":null,"children":[{"id":{"peer":0,"counter":3},"meta":{"Container":{"Normal":{"peer":0,"counter":3,"container_type":"Map"}}},"parent":{"peer":0,"counter":0},"children":[]}]}]}"# ) } } diff --git a/crates/loro-internal/src/txn.rs b/crates/loro-internal/src/txn.rs index 7669e3e5..58ea5bed 100644 --- a/crates/loro-internal/src/txn.rs +++ b/crates/loro-internal/src/txn.rs @@ -94,7 +94,7 @@ pub(super) enum EventHint { key: InternalString, value: Option, }, - Tree(SmallVec<[TreeDiffItem; 2]>), + Tree(TreeDiffItem), MarkEnd, } @@ -579,9 +579,11 @@ fn change_to_diff( )), }), EventHint::Tree(tree_diff) => { + let mut diff = TreeDiff::default(); + diff.push(tree_diff); ans.push(TxnContainerDiff { idx: op.container, - diff: Diff::Tree(TreeDiff::default().extend(tree_diff)), + diff: Diff::Tree(diff), }); } EventHint::MarkEnd => { diff --git a/crates/loro-internal/src/value.rs b/crates/loro-internal/src/value.rs index 758fdf24..4a552ab1 100644 --- a/crates/loro-internal/src/value.rs +++ b/crates/loro-internal/src/value.rs @@ -328,56 +328,35 @@ pub mod wasm { match value { Index::Key(key) => JsValue::from_str(&key), Index::Seq(num) => JsValue::from_f64(num as f64), - Index::Node(node) => JsValue::from_str(&node.to_string()), + Index::Node(node) => node.into(), } } } - impl From for JsValue { - fn from(value: TreeExternalDiff) -> Self { - let obj = Object::new(); - match value { - TreeExternalDiff::Delete => { - js_sys::Reflect::set( - &obj, - &JsValue::from_str("type"), - &JsValue::from_str("delete"), - ) - .unwrap(); - } - TreeExternalDiff::Move(parent) => { - js_sys::Reflect::set( - &obj, - &JsValue::from_str("type"), - &JsValue::from_str("move"), - ) - .unwrap(); - - js_sys::Reflect::set(&obj, &JsValue::from_str("parent"), &parent.into()) - .unwrap(); - } - - TreeExternalDiff::Create => { - js_sys::Reflect::set( - &obj, - &JsValue::from_str("type"), - &JsValue::from_str("create"), - ) - .unwrap(); - } - } - obj.into_js_result().unwrap() - } - } - impl From for JsValue { fn from(value: TreeDiff) -> Self { - let obj = Object::new(); + let array = Array::new(); for diff in value.diff.into_iter() { + let obj = Object::new(); js_sys::Reflect::set(&obj, &"target".into(), &diff.target.into()).unwrap(); - js_sys::Reflect::set(&obj, &"action".into(), &diff.action.into()).unwrap(); + match diff.action { + TreeExternalDiff::Create(p) => { + js_sys::Reflect::set(&obj, &"action".into(), &"create".into()).unwrap(); + js_sys::Reflect::set(&obj, &"parent".into(), &p.to_tree_id().into()) + .unwrap(); + } + TreeExternalDiff::Delete => { + js_sys::Reflect::set(&obj, &"action".into(), &"delete".into()).unwrap(); + } + TreeExternalDiff::Move(p) => { + js_sys::Reflect::set(&obj, &"action".into(), &"move".into()).unwrap(); + js_sys::Reflect::set(&obj, &"parent".into(), &p.to_tree_id().into()) + .unwrap(); + } + } + array.push(&obj); } - obj.into_js_result().unwrap() + array.into_js_result().unwrap() } } diff --git a/crates/loro-wasm/src/lib.rs b/crates/loro-wasm/src/lib.rs index 8cf080dc..33260bb6 100644 --- a/crates/loro-wasm/src/lib.rs +++ b/crates/loro-wasm/src/lib.rs @@ -10,8 +10,7 @@ use loro_internal::{ id::{Counter, TreeID, ID}, obs::SubID, version::Frontiers, - ContainerType, DiffEvent, LoroDoc, LoroError, LoroValue, - VersionVector as InternalVersionVector, + ContainerType, DiffEvent, LoroDoc, LoroValue, VersionVector as InternalVersionVector, }; use rle::HasLength; use serde::{Deserialize, Serialize}; @@ -1851,6 +1850,98 @@ pub struct LoroTree { doc: Arc, } +#[wasm_bindgen] +pub struct LoroTreeNode { + id: TreeID, + tree: TreeHandler, + doc: Arc, +} + +#[wasm_bindgen] +impl LoroTreeNode { + fn from_tree(id: TreeID, tree: TreeHandler, doc: Arc) -> Self { + Self { id, tree, doc } + } + + /// The TreeID of the node. + #[wasm_bindgen(getter)] + pub fn id(&self) -> JsTreeID { + let value: JsValue = self.id.into(); + value.into() + } + + /// Create a new tree node as the child of this node and return a LoroTreeNode instance. + /// + /// @example + /// ```ts + /// import { Loro } from "loro-crdt"; + /// const doc = new Loro(); + /// const tree = doc.getTree("tree"); + /// const root = tree.createNode(); + /// const node = root.createNode(); + /// ``` + #[wasm_bindgen(js_name = "createNode")] + pub fn create_node(&self) -> JsResult { + let id = self.tree.create(Some(self.id))?; + let node = LoroTreeNode::from_tree(id, self.tree.clone(), self.doc.clone()); + Ok(node) + } + + // wasm_bindgen doesn't support Option<&T>, so the move function is split into two functions. + // Or we could use https://docs.rs/wasm-bindgen-derive/latest/wasm_bindgen_derive/#optional-arguments + /// Move the target tree node to be a root node. + #[wasm_bindgen(js_name = "setAsRoot")] + pub fn set_as_root(&self) -> JsResult<()> { + self.tree.mov(self.id, None)?; + Ok(()) + } + + /// Move the target tree node to be a child of the parent. + /// If the parent is undefined, the target will be a root node. + /// + /// @example + /// ```ts + /// const doc = new Loro(); + /// const tree = doc.getTree("tree"); + /// const root = tree.createNode(); + /// const node = root.createNode(); + /// const node2 = node.createNode(); + /// node2.moveTo(root); + /// ``` + #[wasm_bindgen(js_name = "moveTo")] + pub fn move_to(&self, parent: &LoroTreeNode) -> JsResult<()> { + self.tree.mov(self.id, parent.id)?; + Ok(()) + } + + /// Get the associated metadata map container of a tree node. + #[wasm_bindgen(getter)] + pub fn data(&self) -> JsResult { + let data = self.tree.get_meta(self.id)?; + let map = LoroMap { + handler: data, + doc: self.doc.clone(), + }; + Ok(map) + } + + /// Get the parent node of this node. + pub fn parent(&self) -> Option { + let parent = self.tree.parent(self.id).flatten(); + parent.map(|p| LoroTreeNode::from_tree(p, self.tree.clone(), self.doc.clone())) + } + + /// Get the children of this node. + pub fn children(&self) -> Array { + let children = self.tree.children(self.id); + 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) + } +} + #[wasm_bindgen] impl LoroTree { /// "Tree" @@ -1867,8 +1958,9 @@ impl LoroTree { /// /// const doc = new Loro(); /// const tree = doc.getTree("tree"); - /// const root = tree.create(); - /// const node = tree.create(root); + /// const root = tree.createNode(); + /// const node = root.createNode(); + /// console.log(tree.value); /// /* /// [ /// { @@ -1883,18 +1975,18 @@ impl LoroTree { /// } /// ] /// *\/ - /// console.log(tree.value); /// ``` - pub fn create(&mut self, parent: Option) -> JsResult { + #[wasm_bindgen(js_name = "createNode")] + pub fn create_node(&mut self, parent: Option) -> JsResult { let id = if let Some(p) = parent { - let parent: JsValue = p.into(); - let parent: TreeID = parent.try_into().unwrap_throw(); - self.handler.create(parent)? + let p: JsValue = p.into(); + let p = TreeID::try_from(p).unwrap(); + self.handler.create(p)? } else { self.handler.create(None)? }; - let js_id: JsValue = id.into(); - Ok(js_id.into()) + let node = LoroTreeNode::from_tree(id, self.handler.clone(), self.doc.clone()); + Ok(node) } /// Move the target tree node to be a child of the parent. @@ -1907,13 +1999,14 @@ impl LoroTree { /// /// const doc = new Loro(); /// const tree = doc.getTree("tree"); - /// const root = tree.create(); - /// const node = tree.create(root); - /// const node2 = tree.create(node); - /// tree.mov(node2, root); + /// const root = tree.createNode(); + /// const node = root.createNode(); + /// const node2 = node.createNode(); + /// tree.move(node2.id, root.id); /// // Error will be thrown if move operation creates a cycle - /// tree.mov(root, node); + /// tree.move(root.id, node.id); /// ``` + #[wasm_bindgen(js_name = "move")] pub fn mov(&mut self, target: JsTreeID, parent: Option) -> JsResult<()> { let target: JsValue = target.into(); let target = TreeID::try_from(target).unwrap(); @@ -1936,9 +2029,10 @@ impl LoroTree { /// /// const doc = new Loro(); /// const tree = doc.getTree("tree"); - /// const root = tree.create(); - /// const node = tree.create(root); - /// tree.delete(node); + /// const root = tree.createNode(); + /// const node = root.createNode(); + /// tree.delete(node.id); + /// console.log(tree.value); /// /* /// [ /// { @@ -1948,7 +2042,6 @@ impl LoroTree { /// } /// ] /// *\/ - /// console.log(tree.value); /// ``` pub fn delete(&mut self, target: JsTreeID) -> JsResult<()> { let target: JsValue = target.into(); @@ -1956,28 +2049,20 @@ impl LoroTree { Ok(()) } - /// Get the associated metadata map container of a tree node. - /// - /// @example - /// ```ts - /// import { Loro } from "loro-crdt"; - /// - /// const doc = new Loro(); - /// const tree = doc.getTree("tree"); - /// const root = tree.create(); - /// const rootMeta = tree.getMeta(root); - /// rootMeta.set("color", "red"); - /// // [ { id: '0@F2462C4159C4C8D1', parent: null, meta: { color: 'red' } } ] - /// console.log(tree.getDeepValue()); - /// ``` - #[wasm_bindgen(js_name = "getMeta")] - pub fn get_meta(&mut self, target: JsTreeID) -> JsResult { + /// Get LoroTreeNode by the TreeID. + #[wasm_bindgen(js_name = "getNodeByID")] + pub fn get_node_by_id(&self, target: JsTreeID) -> Option { let target: JsValue = target.into(); - let meta = self.handler.get_meta(target.try_into().unwrap())?; - Ok(LoroMap { - handler: meta, - doc: self.doc.clone(), - }) + let target = TreeID::try_from(target).ok()?; + if self.handler.contains(target) { + Some(LoroTreeNode::from_tree( + target, + self.handler.clone(), + self.doc.clone(), + )) + } else { + None + } } /// Get the id of the container. @@ -2011,13 +2096,12 @@ impl LoroTree { /// /// const doc = new Loro(); /// const tree = doc.getTree("tree"); - /// const root = tree.create(); - /// const rootMeta = tree.getMeta(root); - /// rootMeta.set("color", "red"); + /// const root = tree.createNode(); + /// root.data.set("color", "red"); /// // [ { id: '0@F2462C4159C4C8D1', parent: null, meta: 'cid:0@F2462C4159C4C8D1:Map' } ] /// console.log(tree.value); /// // [ { id: '0@F2462C4159C4C8D1', parent: null, meta: { color: 'red' } } ] - /// console.log(tree.getDeepValue()); + /// console.log(tree.toJson()); /// ``` #[wasm_bindgen(js_name = "toJson")] pub fn to_json(&self) -> JsValue { @@ -2032,9 +2116,9 @@ impl LoroTree { /// /// const doc = new Loro(); /// const tree = doc.getTree("tree"); - /// const root = tree.create(); - /// const node = tree.create(root); - /// const node2 = tree.create(node); + /// const root = tree.createNode(); + /// const node = root.createNode(); + /// const node2 = node.createNode(); /// console.log(tree.nodes) // [ '1@A5024AE0E00529D2', '2@A5024AE0E00529D2', '0@A5024AE0E00529D2' ] /// ``` #[wasm_bindgen(js_name = "nodes", method, getter)] @@ -2049,41 +2133,23 @@ impl LoroTree { .collect() } - /// Get the parent of the specific node. - /// Return undefined if the target is a root node. - /// - /// @example - /// ```ts - /// import { Loro } from "loro-crdt"; - /// - /// const doc = new Loro(); - /// const tree = doc.getTree("tree"); - /// const root = tree.create(); - /// const node = tree.create(root); - /// const node2 = tree.create(node); - /// console.log(tree.parent(node2)) // '1@B75DEC6222870A0' - /// console.log(tree.parent(root)) // undefined - /// ``` - pub fn parent(&mut self, target: JsTreeID) -> JsResult> { - let target: JsValue = target.into(); - let id = target - .try_into() - .map_err(|_| LoroError::JsError("parse `TreeID` string error".into()))?; - self.handler - .parent(id) - .map(|p| { - p.map(|p| { - let v: JsValue = p.into(); - v.into() - }) - }) - .ok_or(format!("Tree node `{}` doesn't exist", id).into()) - } - /// Subscribe to the changes of the tree. /// /// returns a subscription id, which can be used to unsubscribe. /// + /// Trees have three types of events: `create`, `delete`, and `move`. + /// - `create`: Creates a new node with its `target` TreeID. If `parent` is undefined, + /// a root node is created; otherwise, a child node of `parent` is created. + /// If the node being created was previously deleted and has archived child nodes, + /// create events for these child nodes will also be received. + /// - `delete`: Deletes the target node. The structure and state of the target node and + /// its child nodes are archived, and delete events for the child nodes will not be received. + /// - `move`: Moves the target node. If `parent` is undefined, the target node becomes a root node; + /// otherwise, it becomes a child node of `parent`. + /// + /// If a tree container is subscribed, the event of metadata changes will also be received as a MapDiff. + /// And event's `path` will end with `TreeID`. + /// /// @example /// ```ts /// import { Loro } from "loro-crdt"; @@ -2091,10 +2157,10 @@ impl LoroTree { /// const doc = new Loro(); /// const tree = doc.getTree("tree"); /// tree.subscribe((event)=>{ - /// console.log(event); + /// // event.type: "create" | "delete" | "move" /// }); - /// const root = tree.create(); - /// const node = tree.create(root); + /// const root = tree.createNode(); + /// const node = root.createNode(); /// doc.commit(); /// ``` pub fn subscribe(&self, loro: &Loro, f: js_sys::Function) -> JsResult { @@ -2120,8 +2186,8 @@ impl LoroTree { /// const subscription = tree.subscribe((event)=>{ /// console.log(event); /// }); - /// const root = tree.create(); - /// const node = tree.create(root); + /// const root = tree.createNode(); + /// const node = root.createNode(); /// doc.commit(); /// tree.unsubscribe(doc, subscription); /// ``` diff --git a/loro-js/src/index.ts b/loro-js/src/index.ts index a013347a..505a9e80 100644 --- a/loro-js/src/index.ts +++ b/loro-js/src/index.ts @@ -1,5 +1,6 @@ export * from "loro-wasm"; -import { Container, Delta, LoroText, LoroTree, OpId, Value, ContainerID, Loro, LoroList, LoroMap, TreeID } from "loro-wasm"; +import { Container, Delta, LoroText, LoroTree,LoroTreeNode, OpId, Value, ContainerID, Loro, LoroList, LoroMap, TreeID } from "loro-wasm"; + Loro.prototype.getTypedMap = function (...args) { @@ -35,11 +36,11 @@ export type Frontiers = OpId[]; /** * Represents a path to identify the exact location of an event's target. - * The path is composed of numbers (e.g., indices of a list container) and strings - * (e.g., keys of a map container), indicating the absolute position of the event's source - * within a loro document. + * The path is composed of numbers (e.g., indices of a list container) strings + * (e.g., keys of a map container) and TreeID (the node of a tree container), + * indicating the absolute position of the event's source within a loro document. */ -export type Path = (number | string)[]; +export type Path = (number | string | TreeID )[]; /** * The event of Loro. @@ -84,11 +85,13 @@ export type MapDiff = { updated: Record; }; +export type TreeDiffItem = { target: TreeID; action: "create"; parent: TreeID | undefined } + | { target: TreeID; action: "delete" } + | { target: TreeID; action: "move"; parent: TreeID | undefined }; + export type TreeDiff = { type: "tree"; - diff: - | { target: TreeID; action: "create" | "delete" } - | { target: TreeID; action: "move"; parent: TreeID }; + diff: TreeDiffItem[]; }; export type Diff = ListDiff | TextDiff | MapDiff | TreeDiff; @@ -213,12 +216,20 @@ declare module "loro-wasm" { } interface LoroTree { - create(parent: TreeID | undefined): TreeID; - mov(target: TreeID, parent: TreeID | undefined): void; + createNode(parent: TreeID | undefined): LoroTreeNode; + move(target: TreeID, parent: TreeID | undefined): void; delete(target: TreeID): void; - getMeta(target: TreeID): LoroMap; - parent(target: TreeID): TreeID | undefined; - contains(target: TreeID): boolean; + has(target: TreeID): boolean; + getNodeByID(target: TreeID): LoroTreeNode; subscribe(txn: Loro, listener: Listener): number; } + + interface LoroTreeNode{ + readonly data: LoroMap; + createNode(): LoroTreeNode; + setAsRoot(): void; + moveTo(parent: LoroTreeNode): void; + parent(): LoroTreeNode | undefined; + children(): Array; + } } diff --git a/loro-js/tests/event.test.ts b/loro-js/tests/event.test.ts index c3ae182c..e38ee67e 100644 --- a/loro-js/tests/event.test.ts +++ b/loro-js/tests/event.test.ts @@ -1,13 +1,13 @@ import { describe, expect, expectTypeOf, it } from "vitest"; import { Delta, + getType, ListDiff, Loro, - LoroText, LoroEvent, + LoroText, MapDiff, TextDiff, - getType, } from "../src"; describe("event", () => { @@ -158,6 +158,21 @@ describe("event", () => { } as MapDiff); }); + it("tree", async () => { + const loro = new Loro(); + let lastEvent: undefined | LoroEvent; + loro.subscribe((event) => { + console.log(event); + lastEvent = event; + }); + const tree = loro.getTree("tree"); + const id = tree.id; + tree.createNode(); + loro.commit(); + await oneMs(); + expect(lastEvent?.target).toEqual(id); + }); + describe("subscribe container events", () => { it("text", async () => { const loro = new Loro(); @@ -311,7 +326,7 @@ describe("event", () => { list.insertContainer(0, "Text"); loro.commit(); await oneMs(); - expect(loro.toJson().list[0]).toBe('abc'); + expect(loro.toJson().list[0]).toBe("abc"); }); }); @@ -319,27 +334,27 @@ describe("event", () => { const doc = new Loro(); const list = doc.getList("list"); let ran = false; - doc.subscribe(event => { + doc.subscribe((event) => { if (event.diff.type === "list") { for (const item of event.diff.diff) { const t = item.insert![0] as LoroText; - expect(t.toString()).toBe("Hello") + expect(t.toString()).toBe("Hello"); expect(item.insert?.length).toBe(2); - expect(getType(item.insert![0])).toBe("Text") - expect(getType(item.insert![1])).toBe("Map") + expect(getType(item.insert![0])).toBe("Text"); + expect(getType(item.insert![1])).toBe("Map"); } ran = true; } - }) + }); list.insertContainer(0, "Map"); const t = list.insertContainer(0, "Text"); t.insert(0, "He"); t.insert(2, "llo"); doc.commit(); - await new Promise(resolve => setTimeout(resolve, 1)); - expect(ran).toBeTruthy() - }) + await new Promise((resolve) => setTimeout(resolve, 1)); + expect(ran).toBeTruthy(); + }); }); function oneMs(): Promise { diff --git a/loro-js/tests/misc.test.ts b/loro-js/tests/misc.test.ts index 0d559f28..ca5ecc90 100644 --- a/loro-js/tests/misc.test.ts +++ b/loro-js/tests/misc.test.ts @@ -254,17 +254,26 @@ describe("tree", () => { const loro = new Loro(); const tree = loro.getTree("root"); - it("create move", () => { - const id = tree.create(); - const childID = tree.create(id); - assertEquals(tree.parent(childID), id); + it("create", () => { + const root = tree.createNode(); + const child = root.createNode(); + assertEquals(child.parent()!.id, root.id); }); + it("move",()=>{ + const root = tree.createNode(); + const child = root.createNode(); + const child2 = root.createNode(); + assertEquals(child2.parent()!.id, root.id); + child2.moveTo(child); + assertEquals(child2.parent()!.id, child.id); + assertEquals(child.children()[0].id, child2.id); + }) + it("meta", () => { - const id = tree.create(); - const meta = tree.getMeta(id); - meta.set("a", 123); - assertEquals(meta.get("a"), 123); + const root = tree.createNode(); + root.data.set("a", 123); + assertEquals(root.data.get("a"), 123); }); });