mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
chore: compose operation
This commit is contained in:
parent
4b654de2e6
commit
01589c6f94
@ -1,7 +1,7 @@
|
|||||||
use crate::entities::{EditParams, EditPayloadPB, ExportDataPB, ExportParams, ExportPayloadPB, TextBlockPB};
|
use crate::entities::{EditParams, EditPayloadPB, ExportDataPB, ExportParams, ExportPayloadPB, TextBlockPB};
|
||||||
use crate::TextEditorManager;
|
use crate::TextEditorManager;
|
||||||
use flowy_error::FlowyError;
|
use flowy_error::FlowyError;
|
||||||
use flowy_sync::entities::text_block::{TextBlockDeltaPB, TextBlockIdPB};
|
use flowy_sync::entities::text_block::TextBlockIdPB;
|
||||||
use lib_dispatch::prelude::{data_result, AppData, Data, DataResult};
|
use lib_dispatch::prelude::{data_result, AppData, Data, DataResult};
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::entities::{EditParams, EditPayloadPB};
|
use crate::entities::EditParams;
|
||||||
use crate::queue::TextBlockRevisionCompactor;
|
use crate::queue::TextBlockRevisionCompactor;
|
||||||
use crate::{editor::TextBlockEditor, errors::FlowyError, TextEditorCloudService};
|
use crate::{editor::TextBlockEditor, errors::FlowyError, TextEditorCloudService};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
|
@ -7,7 +7,7 @@ edition = "2018"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bytecount = "0.6.0"
|
bytecount = "0.6.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||||
#protobuf = {version = "2.18.0"}
|
#protobuf = {version = "2.18.0"}
|
||||||
#flowy-derive = { path = "../flowy-derive" }
|
#flowy-derive = { path = "../flowy-derive" }
|
||||||
tokio = { version = "1", features = ["sync"] }
|
tokio = { version = "1", features = ["sync"] }
|
||||||
|
@ -3,6 +3,7 @@ use crate::core::document::path::Path;
|
|||||||
use crate::core::{Node, NodeBodyChangeset, NodeData, NodeOperation, OperationTransform, Transaction};
|
use crate::core::{Node, NodeBodyChangeset, NodeData, NodeOperation, OperationTransform, Transaction};
|
||||||
use crate::errors::{ErrorBuilder, OTError, OTErrorCode};
|
use crate::errors::{ErrorBuilder, OTError, OTErrorCode};
|
||||||
use indextree::{Arena, Children, FollowingSiblings, NodeId};
|
use indextree::{Arena, Children, FollowingSiblings, NodeId};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use super::NodeOperationList;
|
use super::NodeOperationList;
|
||||||
|
|
||||||
@ -26,14 +27,13 @@ impl NodeTree {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_bytes(root_name: &str, bytes: Vec<u8>) -> Result<Self, OTError> {
|
pub fn from_bytes(root_name: &str, bytes: Vec<u8>) -> Result<Self, OTError> {
|
||||||
let operations = NodeOperationList::from_bytes(bytes)?.into_inner();
|
let operations = NodeOperationList::from_bytes(bytes)?;
|
||||||
Self::from_operations(root_name, operations)
|
Self::from_operations(root_name, operations)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_operations(root_name: &str, operations: Vec<NodeOperation>) -> Result<Self, OTError> {
|
pub fn from_operations(root_name: &str, operations: NodeOperationList) -> Result<Self, OTError> {
|
||||||
let mut node_tree = NodeTree::new(root_name);
|
let mut node_tree = NodeTree::new(root_name);
|
||||||
|
for operation in operations.into_inner().into_iter() {
|
||||||
for operation in operations {
|
|
||||||
let _ = node_tree.apply_op(operation)?;
|
let _ = node_tree.apply_op(operation)?;
|
||||||
}
|
}
|
||||||
Ok(node_tree)
|
Ok(node_tree)
|
||||||
@ -54,13 +54,14 @@ impl NodeTree {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
/// use std::rc::Rc;
|
||||||
/// use lib_ot::core::{NodeOperation, NodeTree, NodeData, Path};
|
/// use lib_ot::core::{NodeOperation, NodeTree, NodeData, Path};
|
||||||
/// let nodes = vec![NodeData::new("text".to_string())];
|
/// let nodes = vec![NodeData::new("text".to_string())];
|
||||||
/// let root_path: Path = vec![0].into();
|
/// let root_path: Path = vec![0].into();
|
||||||
/// let op = NodeOperation::Insert {path: root_path.clone(),nodes };
|
/// let op = NodeOperation::Insert {path: root_path.clone(),nodes };
|
||||||
///
|
///
|
||||||
/// let mut node_tree = NodeTree::new("root");
|
/// let mut node_tree = NodeTree::new("root");
|
||||||
/// node_tree.apply_op(op).unwrap();
|
/// node_tree.apply_op(Rc::new(op)).unwrap();
|
||||||
/// let node_id = node_tree.node_id_at_path(&root_path).unwrap();
|
/// let node_id = node_tree.node_id_at_path(&root_path).unwrap();
|
||||||
/// let node_path = node_tree.path_from_node_id(node_id);
|
/// let node_path = node_tree.path_from_node_id(node_id);
|
||||||
/// debug_assert_eq!(node_path, root_path);
|
/// debug_assert_eq!(node_path, root_path);
|
||||||
@ -105,23 +106,25 @@ impl NodeTree {
|
|||||||
counter
|
counter
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
/// Returns the note_id at the position of the tree with id note_id
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `node_id`:
|
/// * `node_id`: the node id of the child's parent
|
||||||
/// * `index`:
|
/// * `index`: index of the node in parent children list
|
||||||
///
|
///
|
||||||
/// returns: Option<NodeId>
|
/// returns: Option<NodeId>
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
/// use std::rc::Rc;
|
||||||
/// use lib_ot::core::{NodeOperation, NodeTree, NodeData, Path};
|
/// use lib_ot::core::{NodeOperation, NodeTree, NodeData, Path};
|
||||||
/// let node_1 = NodeData::new("text".to_string());
|
/// let node_1 = NodeData::new("text".to_string());
|
||||||
/// let inserted_path: Path = vec![0].into();
|
/// let inserted_path: Path = vec![0].into();
|
||||||
///
|
///
|
||||||
/// let mut node_tree = NodeTree::new("root");
|
/// let mut node_tree = NodeTree::new("root");
|
||||||
/// node_tree.apply_op(NodeOperation::Insert {path: inserted_path.clone(),nodes: vec![node_1.clone()] }).unwrap();
|
/// let op = NodeOperation::Insert {path: inserted_path.clone(),nodes: vec![node_1.clone()] };
|
||||||
|
/// node_tree.apply_op(Rc::new(op)).unwrap();
|
||||||
///
|
///
|
||||||
/// let node_2 = node_tree.get_node_at_path(&inserted_path).unwrap();
|
/// let node_2 = node_tree.get_node_at_path(&inserted_path).unwrap();
|
||||||
/// assert_eq!(node_2.node_type, node_1.node_type);
|
/// assert_eq!(node_2.node_type, node_1.node_type);
|
||||||
@ -137,6 +140,10 @@ impl NodeTree {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns all children whose parent node id is node_id
|
||||||
|
///
|
||||||
|
/// * `node_id`: the children's parent node id
|
||||||
|
///
|
||||||
pub fn children_from_node(&self, node_id: NodeId) -> Children<'_, Node> {
|
pub fn children_from_node(&self, node_id: NodeId) -> Children<'_, Node> {
|
||||||
node_id.children(&self.arena)
|
node_id.children(&self.arena)
|
||||||
}
|
}
|
||||||
@ -159,7 +166,7 @@ impl NodeTree {
|
|||||||
node_id.following_siblings(&self.arena)
|
node_id.following_siblings(&self.arena)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply(&mut self, transaction: Transaction) -> Result<(), OTError> {
|
pub fn apply_transaction(&mut self, transaction: Transaction) -> Result<(), OTError> {
|
||||||
let operations = transaction.into_operations();
|
let operations = transaction.into_operations();
|
||||||
for operation in operations {
|
for operation in operations {
|
||||||
self.apply_op(operation)?;
|
self.apply_op(operation)?;
|
||||||
@ -167,10 +174,15 @@ impl NodeTree {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_op(&mut self, op: NodeOperation) -> Result<(), OTError> {
|
pub fn apply_op(&mut self, op: Rc<NodeOperation>) -> Result<(), OTError> {
|
||||||
|
let op = match Rc::try_unwrap(op) {
|
||||||
|
Ok(op) => op,
|
||||||
|
Err(op) => op.as_ref().clone(),
|
||||||
|
};
|
||||||
|
|
||||||
match op {
|
match op {
|
||||||
NodeOperation::Insert { path, nodes } => self.insert_nodes(&path, nodes),
|
NodeOperation::Insert { path, nodes } => self.insert_nodes(&path, nodes),
|
||||||
NodeOperation::UpdateAttributes { path, attributes, .. } => self.update_attributes(&path, attributes),
|
NodeOperation::UpdateAttributes { path, new, .. } => self.update_attributes(&path, new),
|
||||||
NodeOperation::UpdateBody { path, changeset } => self.update_body(&path, changeset),
|
NodeOperation::UpdateBody { path, changeset } => self.update_body(&path, changeset),
|
||||||
NodeOperation::Delete { path, nodes } => self.delete_node(&path, nodes),
|
NodeOperation::Delete { path, nodes } => self.delete_node(&path, nodes),
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
use crate::core::attributes::Attributes;
|
use crate::core::attributes::Attributes;
|
||||||
use crate::core::document::path::Path;
|
use crate::core::document::path::Path;
|
||||||
use crate::core::{NodeBodyChangeset, NodeData, OperationTransform};
|
use crate::core::{NodeBodyChangeset, NodeData};
|
||||||
use crate::errors::OTError;
|
use crate::errors::OTError;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(tag = "op")]
|
#[serde(tag = "op")]
|
||||||
@ -10,12 +11,11 @@ pub enum NodeOperation {
|
|||||||
#[serde(rename = "insert")]
|
#[serde(rename = "insert")]
|
||||||
Insert { path: Path, nodes: Vec<NodeData> },
|
Insert { path: Path, nodes: Vec<NodeData> },
|
||||||
|
|
||||||
#[serde(rename = "update")]
|
#[serde(rename = "update-attribute")]
|
||||||
UpdateAttributes {
|
UpdateAttributes {
|
||||||
path: Path,
|
path: Path,
|
||||||
attributes: Attributes,
|
new: Attributes,
|
||||||
#[serde(rename = "oldAttributes")]
|
old: Attributes,
|
||||||
old_attributes: Attributes,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
#[serde(rename = "update-body")]
|
#[serde(rename = "update-body")]
|
||||||
@ -27,35 +27,6 @@ pub enum NodeOperation {
|
|||||||
Delete { path: Path, nodes: Vec<NodeData> },
|
Delete { path: Path, nodes: Vec<NodeData> },
|
||||||
}
|
}
|
||||||
|
|
||||||
// impl OperationTransform for NodeOperation {
|
|
||||||
// fn compose(&self, other: &Self) -> Result<Self, OTError>
|
|
||||||
// where
|
|
||||||
// Self: Sized,
|
|
||||||
// {
|
|
||||||
// match self {
|
|
||||||
// NodeOperation::Insert { path, nodes } => {
|
|
||||||
// let new_path = Path::transform(path, other.path(), nodes.len() as i64);
|
|
||||||
// Ok((self.clone(), other.clone_with_new_path(new_path)))
|
|
||||||
// }
|
|
||||||
// NodeOperation::Delete { path, nodes } => {
|
|
||||||
// let new_path = Path::transform(path, other.path(), nodes.len() as i64);
|
|
||||||
// other.clone_with_new_path(new_path)
|
|
||||||
// }
|
|
||||||
// _ => other.clone(),
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// fn transform(&self, other: &Self) -> Result<(Self, Self), OTError>
|
|
||||||
// where
|
|
||||||
// Self: Sized,
|
|
||||||
// {
|
|
||||||
// todo!()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// fn invert(&self, other: &Self) -> Self {
|
|
||||||
// todo!()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
impl NodeOperation {
|
impl NodeOperation {
|
||||||
pub fn get_path(&self) -> &Path {
|
pub fn get_path(&self) -> &Path {
|
||||||
match self {
|
match self {
|
||||||
@ -83,12 +54,12 @@ impl NodeOperation {
|
|||||||
},
|
},
|
||||||
NodeOperation::UpdateAttributes {
|
NodeOperation::UpdateAttributes {
|
||||||
path,
|
path,
|
||||||
attributes,
|
new: attributes,
|
||||||
old_attributes,
|
old: old_attributes,
|
||||||
} => NodeOperation::UpdateAttributes {
|
} => NodeOperation::UpdateAttributes {
|
||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
attributes: old_attributes.clone(),
|
new: old_attributes.clone(),
|
||||||
old_attributes: attributes.clone(),
|
old: attributes.clone(),
|
||||||
},
|
},
|
||||||
NodeOperation::Delete { path, nodes } => NodeOperation::Insert {
|
NodeOperation::Delete { path, nodes } => NodeOperation::Insert {
|
||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
@ -101,10 +72,10 @@ impl NodeOperation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Make the `other` operation to be applied to the version that has been modified.
|
/// Make the `other` operation can be applied to the version after applying the `self` operation.
|
||||||
/// The semantics of transform is used when editing conflicts occur, which is often determined by the version id。
|
/// The semantics of transform is used when editing conflicts occur, which is often determined by the version id。
|
||||||
/// For example, if the inserted position has been acquired by others, then you need to do transform to make sure
|
/// For example, if the inserted position has been acquired by others, then it's needed to do the transform to
|
||||||
/// your position is value.
|
/// make sure the inserted position is right.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
@ -143,24 +114,30 @@ impl NodeOperation {
|
|||||||
let new_path = path.transform(other.get_path(), nodes.len());
|
let new_path = path.transform(other.get_path(), nodes.len());
|
||||||
*other.get_mut_path() = new_path;
|
*other.get_mut_path() = new_path;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {
|
||||||
|
// Only insert/delete will change the path.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct NodeOperationList {
|
pub struct NodeOperationList {
|
||||||
operations: Vec<NodeOperation>,
|
operations: Vec<Rc<NodeOperation>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NodeOperationList {
|
impl NodeOperationList {
|
||||||
pub fn into_inner(self) -> Vec<NodeOperation> {
|
pub fn into_inner(self) -> Vec<Rc<NodeOperation>> {
|
||||||
self.operations
|
self.operations
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_op(&mut self, operation: NodeOperation) {
|
||||||
|
self.operations.push(Rc::new(operation));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::Deref for NodeOperationList {
|
impl std::ops::Deref for NodeOperationList {
|
||||||
type Target = Vec<NodeOperation>;
|
type Target = Vec<Rc<NodeOperation>>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.operations
|
&self.operations
|
||||||
@ -175,13 +152,15 @@ impl std::ops::DerefMut for NodeOperationList {
|
|||||||
|
|
||||||
impl std::convert::From<Vec<NodeOperation>> for NodeOperationList {
|
impl std::convert::From<Vec<NodeOperation>> for NodeOperationList {
|
||||||
fn from(operations: Vec<NodeOperation>) -> Self {
|
fn from(operations: Vec<NodeOperation>) -> Self {
|
||||||
Self { operations }
|
Self::new(operations)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NodeOperationList {
|
impl NodeOperationList {
|
||||||
pub fn new(operations: Vec<NodeOperation>) -> Self {
|
pub fn new(operations: Vec<NodeOperation>) -> Self {
|
||||||
Self { operations }
|
Self {
|
||||||
|
operations: operations.into_iter().map(Rc::new).collect(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, OTError> {
|
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, OTError> {
|
||||||
|
@ -1,6 +1,28 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// The `Path` represents as a path to reference to the node in the `NodeTree`.
|
/// The `Path` represents as a path to reference to the node in the `NodeTree`.
|
||||||
|
/// ┌─────────┐
|
||||||
|
/// │ Root │
|
||||||
|
/// └─────────┼──────────┐
|
||||||
|
/// │0: Node A │
|
||||||
|
/// └──────────┼────────────┐
|
||||||
|
/// │0: Node A-1 │
|
||||||
|
/// ├────────────┤
|
||||||
|
/// │1: Node A-2 │
|
||||||
|
/// ┌──────────┼────────────┘
|
||||||
|
/// │1: Node B │
|
||||||
|
/// └──────────┼────────────┐
|
||||||
|
/// │0: Node B-1 │
|
||||||
|
/// ├────────────┤
|
||||||
|
/// │1: Node B-2 │
|
||||||
|
/// ┌──────────┼────────────┘
|
||||||
|
/// │2: Node C │
|
||||||
|
/// └──────────┘
|
||||||
|
///
|
||||||
|
/// The path of Node A will be [0]
|
||||||
|
/// The path of Node A-1 will be [0,0]
|
||||||
|
/// The path of Node A-2 will be [0,1]
|
||||||
|
/// The path of Node B-2 will be [1,1]
|
||||||
#[derive(Clone, Serialize, Deserialize, Eq, PartialEq, Debug)]
|
#[derive(Clone, Serialize, Deserialize, Eq, PartialEq, Debug)]
|
||||||
pub struct Path(pub Vec<usize>);
|
pub struct Path(pub Vec<usize>);
|
||||||
|
|
||||||
@ -49,21 +71,90 @@ impl From<&[usize]> for Path {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Path {
|
impl Path {
|
||||||
|
/// Calling this function if there are two changes want to modify the same path.
|
||||||
///
|
///
|
||||||
///
|
|
||||||
/// The path will be changed is
|
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `other`:
|
/// * `other`: the path that need to be transformed
|
||||||
/// * `offset`: represents the len of nodes referenced by this path
|
/// * `offset`: represents the len of nodes referenced by the current path
|
||||||
///
|
///
|
||||||
/// returns: Path
|
/// If two changes modify the same path or the path was shared by them. Then it needs to do the
|
||||||
|
/// transformation to make sure the changes are applied to the right path.
|
||||||
|
///
|
||||||
|
/// returns: the path represents the position that the other path reference to.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
/// use lib_ot::core::Path;
|
||||||
|
/// let path = Path(vec![0, 1]);
|
||||||
|
/// for (old_path, len_of_nodes, expected_path) in vec![
|
||||||
|
/// // Try to modify the path [0, 1], but someone has inserted one element before the
|
||||||
|
/// // current path [0,1] in advance. That causes the modified path [0,1] to no longer
|
||||||
|
/// // valid. It needs to do the transformation to get the right path.
|
||||||
|
/// //
|
||||||
|
/// // [0,2] is the path you want to modify.
|
||||||
|
/// (Path(vec![0, 1]), 1, Path(vec![0, 2])),
|
||||||
|
/// (Path(vec![0, 1]), 5, Path(vec![0, 6])),
|
||||||
|
/// (Path(vec![0, 2]), 1, Path(vec![0, 3])),
|
||||||
|
/// // Try to modify the path [0, 2,3,4], but someone has inserted one element before the
|
||||||
|
/// // current path [0,1] in advance. That cause the prefix path [0,2] of [0,2,3,4]
|
||||||
|
/// // no longer valid.
|
||||||
|
/// // It needs to do the transformation to get the right path. So [0,2] is transformed to [0,3]
|
||||||
|
/// // and the suffix [3,4] of the [0,2,3,4] remains the same. So the transformed result is
|
||||||
|
/// //
|
||||||
|
/// // [0,3,3,4]
|
||||||
|
/// (Path(vec![0, 2, 3, 4]), 1, Path(vec![0, 3, 3, 4])),
|
||||||
|
/// ] {
|
||||||
|
/// assert_eq!(path.transform(&old_path, len_of_nodes), expected_path);
|
||||||
|
/// }
|
||||||
|
/// // The path remains the same in the following test. Because the shared path is not changed.
|
||||||
|
/// let path = Path(vec![0, 1, 2]);
|
||||||
|
/// for (old_path, len_of_nodes, expected_path) in vec![
|
||||||
|
/// // Try to modify the path [0,0,0,1,2], but someone has inserted one element
|
||||||
|
/// // before [0,1,2]. [0,0,0,1,2] and [0,1,2] share the same path [0,x], because
|
||||||
|
/// // the element was inserted at [0,1,2] that didn't affect the shared path [0, x].
|
||||||
|
/// // So, after the transformation, the path is not changed.
|
||||||
|
/// (Path(vec![0, 0, 0, 1, 2]), 1, Path(vec![0, 0, 0, 1, 2])),
|
||||||
|
/// (Path(vec![0, 1]), 1, Path(vec![0, 1])),
|
||||||
|
/// ] {
|
||||||
|
/// assert_eq!(path.transform(&old_path, len_of_nodes), expected_path);
|
||||||
|
/// }
|
||||||
///
|
///
|
||||||
|
/// let path = Path(vec![1, 1]);
|
||||||
|
/// for (old_path, len_of_nodes, expected_path) in vec![(Path(vec![1, 0]), 1, Path(vec![1, 0]))] {
|
||||||
|
/// assert_eq!(path.transform(&old_path, len_of_nodes), expected_path);
|
||||||
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
/// For example, client A and client B want to insert a node at the same index, the server applies
|
||||||
|
/// the changes made by client B. But, before applying the client A's changes, server transforms
|
||||||
|
/// the changes first in order to make sure that client A modify the right position. After that,
|
||||||
|
/// the changes can be applied to the server.
|
||||||
|
///
|
||||||
|
/// ┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||||
|
/// │ Client A │ │ Server │ │ Client B │
|
||||||
|
/// └─────┬────┘ └─────┬────┘ └────┬─────┘
|
||||||
|
/// │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ┐ │
|
||||||
|
/// │ │ Root │
|
||||||
|
/// │ │ │ 0:A │ │
|
||||||
|
/// │ │ ─ ─ ─ ─ ─ ─ ─ ─ │
|
||||||
|
/// │ │ ◀───────────────────────│
|
||||||
|
/// │ │ Insert B at index 1 │
|
||||||
|
/// │ │ │
|
||||||
|
/// │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ┐ │
|
||||||
|
/// │ │ Root │
|
||||||
|
/// │ │ │ 0:A │ │
|
||||||
|
/// ├──────────────────────▶│ 1:B │
|
||||||
|
/// │ Insert C at index 1 │ └ ─ ─ ─ ─ ─ ─ ─ ┘ │
|
||||||
|
/// │ │ │
|
||||||
|
/// │ │ transform index 1 to 2 │
|
||||||
|
/// │ │ │
|
||||||
|
/// │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ │
|
||||||
|
/// │ │ Root │ │
|
||||||
|
/// │ │ │ 0:A │
|
||||||
|
/// ▼ ▼ 1:B │ ▼
|
||||||
|
/// │ 2:C
|
||||||
|
/// ─ ─ ─ ─ ─ ─ ─ ─ ┘
|
||||||
pub fn transform(&self, other: &Path, offset: usize) -> Path {
|
pub fn transform(&self, other: &Path, offset: usize) -> Path {
|
||||||
if self.len() > other.len() {
|
if self.len() > other.len() {
|
||||||
return other.clone();
|
return other.clone();
|
||||||
@ -81,7 +172,7 @@ impl Path {
|
|||||||
let second_last_index = self.0.len() - 1;
|
let second_last_index = self.0.len() - 1;
|
||||||
let mut prefix: Vec<usize> = self.0[0..second_last_index].into();
|
let mut prefix: Vec<usize> = self.0[0..second_last_index].into();
|
||||||
let mut suffix: Vec<usize> = other.0[self.0.len()..].into();
|
let mut suffix: Vec<usize> = other.0[self.0.len()..].into();
|
||||||
let last_value = self.0.last().unwrap().clone();
|
let last_value = *self.0.last().unwrap();
|
||||||
|
|
||||||
let other_second_last_value = other.0[second_last_index];
|
let other_second_last_value = other.0[second_last_index];
|
||||||
|
|
||||||
@ -96,81 +187,4 @@ impl Path {
|
|||||||
prefix.append(&mut suffix);
|
prefix.append(&mut suffix);
|
||||||
Path(prefix)
|
Path(prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
// delta is default to be 1
|
|
||||||
pub fn transform3(pre_insert_path: &Path, b: &Path, offset: i64) -> Path {
|
|
||||||
if pre_insert_path.len() > b.len() {
|
|
||||||
return b.clone();
|
|
||||||
}
|
|
||||||
if pre_insert_path.is_empty() || b.is_empty() {
|
|
||||||
return b.clone();
|
|
||||||
}
|
|
||||||
// check the prefix
|
|
||||||
for i in 0..(pre_insert_path.len() - 1) {
|
|
||||||
if pre_insert_path.0[i] != b.0[i] {
|
|
||||||
return b.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut prefix: Vec<usize> = pre_insert_path.0[0..(pre_insert_path.len() - 1)].into();
|
|
||||||
let mut suffix: Vec<usize> = b.0[pre_insert_path.0.len()..].into();
|
|
||||||
let prev_insert_last: usize = *pre_insert_path.0.last().unwrap();
|
|
||||||
let b_at_index = b.0[pre_insert_path.0.len() - 1];
|
|
||||||
if prev_insert_last <= b_at_index {
|
|
||||||
prefix.push(((b_at_index as i64) + offset) as usize);
|
|
||||||
} else {
|
|
||||||
prefix.push(b_at_index);
|
|
||||||
}
|
|
||||||
prefix.append(&mut suffix);
|
|
||||||
|
|
||||||
Path(prefix)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::core::Path;
|
|
||||||
#[test]
|
|
||||||
fn path_transform_test_1() {
|
|
||||||
assert_eq!(
|
|
||||||
{ Path::transform3(&Path(vec![0, 1]), &Path(vec![0, 1]), 1) }.0,
|
|
||||||
vec![0, 2]
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
{ Path::transform3(&Path(vec![0, 1]), &Path(vec![0, 1]), 5) }.0,
|
|
||||||
vec![0, 6]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn path_transform_test_2() {
|
|
||||||
assert_eq!(
|
|
||||||
{ Path::transform3(&Path(vec![0, 1]), &Path(vec![0, 2]), 1) }.0,
|
|
||||||
vec![0, 3]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn path_transform_test_3() {
|
|
||||||
assert_eq!(
|
|
||||||
{ Path::transform3(&Path(vec![0, 1]), &Path(vec![0, 2, 7, 8, 9]), 1) }.0,
|
|
||||||
vec![0, 3, 7, 8, 9]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn path_transform_no_changed_test() {
|
|
||||||
assert_eq!(
|
|
||||||
{ Path::transform3(&Path(vec![0, 1, 2]), &Path(vec![0, 0, 7, 8, 9]), 1) }.0,
|
|
||||||
vec![0, 0, 7, 8, 9]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
{ Path::transform3(&Path(vec![0, 1, 2]), &Path(vec![0, 1]), 1) }.0,
|
|
||||||
vec![0, 1]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
{ Path::transform3(&Path(vec![1, 1]), &Path(vec![1, 0]), 1) }.0,
|
|
||||||
vec![1, 0]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
use crate::core::attributes::Attributes;
|
use crate::core::attributes::Attributes;
|
||||||
use crate::core::document::path::Path;
|
use crate::core::document::path::Path;
|
||||||
use crate::core::{NodeData, NodeOperation, NodeTree};
|
use crate::core::{NodeData, NodeOperation, NodeTree};
|
||||||
|
use crate::errors::OTError;
|
||||||
use indextree::NodeId;
|
use indextree::NodeId;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use super::{NodeBodyChangeset, NodeOperationList};
|
use super::{NodeBodyChangeset, NodeOperationList};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct Transaction {
|
pub struct Transaction {
|
||||||
operations: NodeOperationList,
|
operations: NodeOperationList,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Transaction {
|
impl Transaction {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Transaction {
|
Self::default()
|
||||||
operations: vec![].into(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_operations<T: Into<NodeOperationList>>(operations: T) -> Self {
|
pub fn from_operations<T: Into<NodeOperationList>>(operations: T) -> Self {
|
||||||
@ -23,25 +23,36 @@ impl Transaction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_operations(self) -> Vec<NodeOperation> {
|
pub fn into_operations(self) -> Vec<Rc<NodeOperation>> {
|
||||||
self.operations.into_inner()
|
self.operations.into_inner()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Make the `other` to be applied to the version that has been modified.
|
/// Make the `other` can be applied to the version after applying the `self` transaction.
|
||||||
///
|
///
|
||||||
/// The semantics of transform is used when editing conflicts occur, which is often determined by the version id。
|
/// The semantics of transform is used when editing conflicts occur, which is often determined by the version id。
|
||||||
/// the operations of the transaction will be transformed into the conflict operations.
|
/// the operations of the transaction will be transformed into the conflict operations.
|
||||||
pub fn transform(&self, other: &mut Transaction) {
|
pub fn transform(&self, other: &Transaction) -> Result<Transaction, OTError> {
|
||||||
for other_operation in other.iter_mut() {
|
let mut new_transaction = other.clone();
|
||||||
|
for other_operation in new_transaction.iter_mut() {
|
||||||
|
let other_operation = Rc::make_mut(other_operation);
|
||||||
for operation in self.operations.iter() {
|
for operation in self.operations.iter() {
|
||||||
operation.transform(other_operation);
|
operation.transform(other_operation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(new_transaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compose(&mut self, other: &Transaction) -> Result<(), OTError> {
|
||||||
|
// For the moment, just append `other` operations to the end of `self`.
|
||||||
|
for operation in other.operations.iter() {
|
||||||
|
self.operations.push(operation.clone());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::Deref for Transaction {
|
impl std::ops::Deref for Transaction {
|
||||||
type Target = NodeOperationList;
|
type Target = Vec<Rc<NodeOperation>>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.operations
|
&self.operations
|
||||||
@ -85,7 +96,7 @@ impl<'a> TransactionBuilder<'a> {
|
|||||||
/// let transaction = TransactionBuilder::new(&node_tree)
|
/// let transaction = TransactionBuilder::new(&node_tree)
|
||||||
/// .insert_nodes_at_path(0,vec![ NodeData::new("text_1"), NodeData::new("text_2")])
|
/// .insert_nodes_at_path(0,vec![ NodeData::new("text_1"), NodeData::new("text_2")])
|
||||||
/// .finalize();
|
/// .finalize();
|
||||||
/// node_tree.apply(transaction).unwrap();
|
/// node_tree.apply_transaction(transaction).unwrap();
|
||||||
///
|
///
|
||||||
/// node_tree.node_id_at_path(vec![0, 0]);
|
/// node_tree.node_id_at_path(vec![0, 0]);
|
||||||
/// ```
|
/// ```
|
||||||
@ -115,7 +126,7 @@ impl<'a> TransactionBuilder<'a> {
|
|||||||
/// let transaction = TransactionBuilder::new(&node_tree)
|
/// let transaction = TransactionBuilder::new(&node_tree)
|
||||||
/// .insert_node_at_path(0, NodeData::new("text"))
|
/// .insert_node_at_path(0, NodeData::new("text"))
|
||||||
/// .finalize();
|
/// .finalize();
|
||||||
/// node_tree.apply(transaction).unwrap();
|
/// node_tree.apply_transaction(transaction).unwrap();
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
pub fn insert_node_at_path<T: Into<Path>>(self, path: T, node: NodeData) -> Self {
|
pub fn insert_node_at_path<T: Into<Path>>(self, path: T, node: NodeData) -> Self {
|
||||||
@ -133,10 +144,10 @@ impl<'a> TransactionBuilder<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.operations.push(NodeOperation::UpdateAttributes {
|
self.operations.add_op(NodeOperation::UpdateAttributes {
|
||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
attributes,
|
new: attributes,
|
||||||
old_attributes,
|
old: old_attributes,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
None => tracing::warn!("Update attributes at path: {:?} failed. Node is not exist", path),
|
None => tracing::warn!("Update attributes at path: {:?} failed. Node is not exist", path),
|
||||||
@ -147,7 +158,7 @@ impl<'a> TransactionBuilder<'a> {
|
|||||||
pub fn update_body_at_path(mut self, path: &Path, changeset: NodeBodyChangeset) -> Self {
|
pub fn update_body_at_path(mut self, path: &Path, changeset: NodeBodyChangeset) -> Self {
|
||||||
match self.node_tree.node_id_at_path(path) {
|
match self.node_tree.node_id_at_path(path) {
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
self.operations.push(NodeOperation::UpdateBody {
|
self.operations.add_op(NodeOperation::UpdateBody {
|
||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
changeset,
|
changeset,
|
||||||
});
|
});
|
||||||
@ -169,7 +180,7 @@ impl<'a> TransactionBuilder<'a> {
|
|||||||
node = self.node_tree.following_siblings(node).next().unwrap();
|
node = self.node_tree.following_siblings(node).next().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.operations.push(NodeOperation::Delete {
|
self.operations.add_op(NodeOperation::Delete {
|
||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
nodes: deleted_nodes,
|
nodes: deleted_nodes,
|
||||||
});
|
});
|
||||||
@ -193,7 +204,7 @@ impl<'a> TransactionBuilder<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn push(mut self, op: NodeOperation) -> Self {
|
pub fn push(mut self, op: NodeOperation) -> Self {
|
||||||
self.operations.push(op);
|
self.operations.add_op(op);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::node::script::NodeScript::*;
|
use crate::node::script::NodeScript::*;
|
||||||
use crate::node::script::NodeTest;
|
use crate::node::script::NodeTest;
|
||||||
use lib_ot::core::{AttributeBuilder, Node, NodeTree, Transaction, TransactionBuilder};
|
use lib_ot::core::{AttributeBuilder, Node};
|
||||||
use lib_ot::{
|
use lib_ot::{
|
||||||
core::{NodeBodyChangeset, NodeData, NodeDataBuilder, NodeOperation, Path},
|
core::{NodeBodyChangeset, NodeData, NodeDataBuilder, NodeOperation, Path},
|
||||||
text_delta::TextDeltaBuilder,
|
text_delta::TextDeltaBuilder,
|
||||||
@ -35,15 +35,14 @@ fn operation_insert_node_with_children_serde_test() {
|
|||||||
fn operation_update_node_attributes_serde_test() {
|
fn operation_update_node_attributes_serde_test() {
|
||||||
let operation = NodeOperation::UpdateAttributes {
|
let operation = NodeOperation::UpdateAttributes {
|
||||||
path: Path(vec![0, 1]),
|
path: Path(vec![0, 1]),
|
||||||
attributes: AttributeBuilder::new().insert("bold", true).build(),
|
new: AttributeBuilder::new().insert("bold", true).build(),
|
||||||
old_attributes: AttributeBuilder::new().insert("bold", false).build(),
|
old: AttributeBuilder::new().insert("bold", false).build(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = serde_json::to_string(&operation).unwrap();
|
let result = serde_json::to_string(&operation).unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result,
|
result,
|
||||||
r#"{"op":"update","path":[0,1],"attributes":{"bold":true},"oldAttributes":{"bold":null}}"#
|
r#"{"op":"update-attribute","path":[0,1],"new":{"bold":true},"old":{"bold":null}}"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,17 +118,17 @@ fn operation_insert_transform_one_level_path_test() {
|
|||||||
let scripts = vec![
|
let scripts = vec![
|
||||||
InsertNode {
|
InsertNode {
|
||||||
path: 0.into(),
|
path: 0.into(),
|
||||||
node_data: node_data_1.clone(),
|
node_data: node_data_1,
|
||||||
rev_id: 1,
|
rev_id: 1,
|
||||||
},
|
},
|
||||||
InsertNode {
|
InsertNode {
|
||||||
path: 1.into(),
|
path: 1.into(),
|
||||||
node_data: node_data_2.clone(),
|
node_data: node_data_2,
|
||||||
rev_id: 2,
|
rev_id: 2,
|
||||||
},
|
},
|
||||||
InsertNode {
|
InsertNode {
|
||||||
path: 1.into(),
|
path: 1.into(),
|
||||||
node_data: node_data_3.clone(),
|
node_data: node_data_3,
|
||||||
rev_id: 1,
|
rev_id: 1,
|
||||||
},
|
},
|
||||||
AssertNode {
|
AssertNode {
|
||||||
@ -157,12 +156,12 @@ fn operation_insert_transform_multiple_level_path_test() {
|
|||||||
let scripts = vec![
|
let scripts = vec![
|
||||||
InsertNode {
|
InsertNode {
|
||||||
path: 0.into(),
|
path: 0.into(),
|
||||||
node_data: node_data_1.clone(),
|
node_data: node_data_1,
|
||||||
rev_id: 1,
|
rev_id: 1,
|
||||||
},
|
},
|
||||||
InsertNode {
|
InsertNode {
|
||||||
path: 1.into(),
|
path: 1.into(),
|
||||||
node_data: node_data_2.clone(),
|
node_data: node_data_2,
|
||||||
rev_id: 2,
|
rev_id: 2,
|
||||||
},
|
},
|
||||||
InsertNode {
|
InsertNode {
|
||||||
@ -189,12 +188,12 @@ fn operation_delete_transform_path_test() {
|
|||||||
let scripts = vec![
|
let scripts = vec![
|
||||||
InsertNode {
|
InsertNode {
|
||||||
path: 0.into(),
|
path: 0.into(),
|
||||||
node_data: node_data_1.clone(),
|
node_data: node_data_1,
|
||||||
rev_id: 1,
|
rev_id: 1,
|
||||||
},
|
},
|
||||||
InsertNode {
|
InsertNode {
|
||||||
path: 1.into(),
|
path: 1.into(),
|
||||||
node_data: node_data_2.clone(),
|
node_data: node_data_2,
|
||||||
rev_id: 2,
|
rev_id: 2,
|
||||||
},
|
},
|
||||||
// The node's in the tree will be:
|
// The node's in the tree will be:
|
||||||
@ -206,7 +205,7 @@ fn operation_delete_transform_path_test() {
|
|||||||
// but it was moved to index 2.
|
// but it was moved to index 2.
|
||||||
InsertNode {
|
InsertNode {
|
||||||
path: 1.into(),
|
path: 1.into(),
|
||||||
node_data: node_data_3.clone(),
|
node_data: node_data_3,
|
||||||
rev_id: 3,
|
rev_id: 3,
|
||||||
},
|
},
|
||||||
// 0: text_1
|
// 0: text_1
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use lib_ot::core::{Node, NodeOperation, Transaction};
|
use lib_ot::core::{Node, Transaction};
|
||||||
use lib_ot::{
|
use lib_ot::{
|
||||||
core::attributes::Attributes,
|
core::attributes::Attributes,
|
||||||
core::{NodeBody, NodeBodyChangeset, NodeData, NodeTree, Path, TransactionBuilder},
|
core::{NodeBody, NodeBodyChangeset, NodeData, NodeTree, Path, TransactionBuilder},
|
||||||
@ -144,14 +144,14 @@ impl NodeTest {
|
|||||||
fn apply_transaction(&mut self, transaction: Transaction) {
|
fn apply_transaction(&mut self, transaction: Transaction) {
|
||||||
self.rev_id += 1;
|
self.rev_id += 1;
|
||||||
self.rev_operations.insert(self.rev_id, transaction.clone());
|
self.rev_operations.insert(self.rev_id, transaction.clone());
|
||||||
self.node_tree.apply(transaction).unwrap();
|
self.node_tree.apply_transaction(transaction).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn transform_transaction_if_need(&mut self, transaction: &mut Transaction, rev_id: usize) {
|
fn transform_transaction_if_need(&mut self, transaction: &mut Transaction, rev_id: usize) {
|
||||||
if self.rev_id >= rev_id {
|
if self.rev_id >= rev_id {
|
||||||
for rev_id in rev_id..=self.rev_id {
|
for rev_id in rev_id..=self.rev_id {
|
||||||
let old_transaction = self.rev_operations.get(&rev_id).unwrap();
|
let old_transaction = self.rev_operations.get(&rev_id).unwrap();
|
||||||
old_transaction.transform(transaction);
|
*transaction = old_transaction.transform(transaction).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,7 +114,7 @@ fn node_insert_node_in_ordered_nodes_test() {
|
|||||||
},
|
},
|
||||||
InsertNode {
|
InsertNode {
|
||||||
path: path_3.clone(),
|
path: path_3.clone(),
|
||||||
node_data: node_3.clone(),
|
node_data: node_3,
|
||||||
rev_id: 3,
|
rev_id: 3,
|
||||||
},
|
},
|
||||||
// 0:text_1
|
// 0:text_1
|
||||||
@ -166,12 +166,12 @@ fn node_insert_nested_nodes_test() {
|
|||||||
let scripts = vec![
|
let scripts = vec![
|
||||||
InsertNode {
|
InsertNode {
|
||||||
path: 0.into(),
|
path: 0.into(),
|
||||||
node_data: node_data_1.clone(),
|
node_data: node_data_1,
|
||||||
rev_id: 1,
|
rev_id: 1,
|
||||||
},
|
},
|
||||||
InsertNode {
|
InsertNode {
|
||||||
path: 1.into(),
|
path: 1.into(),
|
||||||
node_data: node_data_2.clone(),
|
node_data: node_data_2,
|
||||||
rev_id: 2,
|
rev_id: 2,
|
||||||
},
|
},
|
||||||
// the tree will be:
|
// the tree will be:
|
||||||
@ -214,7 +214,7 @@ fn node_insert_node_before_existing_nested_nodes_test() {
|
|||||||
let scripts = vec![
|
let scripts = vec![
|
||||||
InsertNode {
|
InsertNode {
|
||||||
path: 0.into(),
|
path: 0.into(),
|
||||||
node_data: node_data_1.clone(),
|
node_data: node_data_1,
|
||||||
rev_id: 1,
|
rev_id: 1,
|
||||||
},
|
},
|
||||||
// 0:text_1
|
// 0:text_1
|
||||||
|
Loading…
x
Reference in New Issue
Block a user