mirror of
https://github.com/loro-dev/loro.git
synced 2024-11-24 20:20:36 +00:00
feat: remove deleted set in tree state and optimize api (#259)
Co-authored-by: Zixuan Chen <me@zxch3n.com>
This commit is contained in:
parent
0bcc3bd56d
commit
dcbdd55195
17 changed files with 647 additions and 678 deletions
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
|
|
@ -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":[]}]}]}"#
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
/// ```
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue