feat: remove deleted set in tree state and optimize api (#259)

Co-authored-by: Zixuan Chen <me@zxch3n.com>
This commit is contained in:
Leon Zhao 2024-01-30 09:54:54 +08:00 committed by GitHub
parent 0bcc3bd56d
commit dcbdd55195
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 647 additions and 678 deletions

View file

@ -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"
}
}

View file

@ -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<TreeID> = 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<TreeID> = 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<Self> {
pub const fn delete_root() -> Self {
DELETED_TREE_ROOT
}
/// return `true` if the `TreeID` is deleted root
pub fn is_deleted_root(target: Option<TreeID>) -> bool {
target == DELETED_TREE_ROOT
}
pub const fn unexist_root() -> Option<Self> {
UNEXIST_TREE_ROOT
}
/// return `true` if the `TreeID` is non-existent root
pub fn is_unexist_root(target: Option<TreeID>) -> bool {
target == UNEXIST_TREE_ROOT
pub fn is_deleted_root(target: &TreeID) -> bool {
target == &DELETED_TREE_ROOT
}
pub fn from_id(id: ID) -> Self {

View file

@ -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<TreeID>,
}
impl TreeOp {
// TODO: use `TreeParentId` instead of `Option<TreeID>`
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

View file

@ -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<TreeID>),
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<TreeDiffItem> {
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<TreeDeltaItem>,
}
/// 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<TreeID>,
old_parent: Option<TreeID>,
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()),
}
}
}

View file

@ -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<TreeID>;
fn deleted_mut(&mut self) -> &mut FxHashSet<TreeID>;
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<TreeID>,
old_parent: Option<TreeID>,
) {
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<TreeID>,
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<std::cmp::Ordering> {
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<H: core::hash::Hasher>(&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<TreeID>, 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));
}
}

View file

@ -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<u64>,
containers: ContainerArena,
keys: Vec<InternalString>,
@ -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<dyn Iterator<Item = EncodedDep> + 'a>,
pub state_blob_arena: &'a [u8],
}

View file

@ -133,7 +133,7 @@ impl DiffVariant {
}
#[non_exhaustive]
#[derive(Clone, Debug, EnumAsInner, Serialize)]
#[derive(Clone, Debug, EnumAsInner)]
pub(crate) enum InternalDiff {
ListRaw(Delta<SliceRanges>),
/// This always uses entity indexes.

View file

@ -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<TreeID> {
let parent = parent.into();
let parent: Option<TreeID> = 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<Option<TreeID>> {
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<TreeID> {
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()
})
}

View file

@ -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};

View file

@ -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};

View file

@ -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<TreeID>) -> 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<TreeID> {
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<TreeID, TreeStateNode>,
pub(crate) deleted: FxHashSet<TreeID>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct TreeStateNode {
pub parent: Option<TreeID>,
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<std::cmp::Ordering> {
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<TreeID>, 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<Option<TreeID>> {
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<TreeParentId> {
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<TreeID> {
self.trees
.keys()
.filter(|&k| !self.is_deleted(k) && !TreeID::is_unexist_root(Some(*k)))
.filter(|&k| !self.is_node_deleted(k))
.copied()
.collect::<Vec<_>>()
}
@ -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<TreeID> {
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<LoroValue> = 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<TreeID> {
&self.deleted
}
fn deleted_mut(&mut self) -> &mut FxHashSet<TreeID> {
&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<TreeNode>,
deleted: Vec<TreeNode>,
// deleted: Vec<TreeNode>,
}
/// 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<TreeID, TreeStateNode>) -> 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":[]}]}]}"#
)
}
}

View file

@ -94,7 +94,7 @@ pub(super) enum EventHint {
key: InternalString,
value: Option<LoroValue>,
},
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 => {

View file

@ -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<TreeExternalDiff> 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<TreeDiff> 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()
}
}

View file

@ -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<LoroDoc>,
}
#[wasm_bindgen]
pub struct LoroTreeNode {
id: TreeID,
tree: TreeHandler,
doc: Arc<LoroDoc>,
}
#[wasm_bindgen]
impl LoroTreeNode {
fn from_tree(id: TreeID, tree: TreeHandler, doc: Arc<LoroDoc>) -> 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<LoroTreeNode> {
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<LoroMap> {
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<LoroTreeNode> {
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<JsTreeID>) -> JsResult<JsTreeID> {
#[wasm_bindgen(js_name = "createNode")]
pub fn create_node(&mut self, parent: Option<JsTreeID>) -> JsResult<LoroTreeNode> {
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<JsTreeID>) -> 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<LoroMap> {
/// Get LoroTreeNode by the TreeID.
#[wasm_bindgen(js_name = "getNodeByID")]
pub fn get_node_by_id(&self, target: JsTreeID) -> Option<LoroTreeNode> {
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<Option<JsTreeID>> {
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<u32> {
@ -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);
/// ```

View file

@ -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<string, Value | Container | undefined>;
};
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<LoroTreeNode>;
}
}

View file

@ -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<void> {

View file

@ -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);
});
});