Merge branch 'dev' into feat-gc

This commit is contained in:
Zixuan Chen 2024-09-11 10:54:29 +08:00
commit 4a27a0645f
No known key found for this signature in database
45 changed files with 985 additions and 731 deletions

View file

@ -4,7 +4,7 @@ on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
branches: ["main", "dev"]
types: [opened, synchronize, reopened, ready_for_review]
env:

1
Cargo.lock generated
View file

@ -1351,6 +1351,7 @@ dependencies = [
"criterion 0.5.1",
"fractional_index",
"imbl",
"once_cell",
"rand",
"serde",
"smallvec",

View file

@ -16,6 +16,7 @@ imbl = "^3.0"
smallvec = { workspace = true }
serde = { workspace = true, features = ["derive", "rc"], optional = true }
rand = { version = "^0.8" }
once_cell = { workspace = true }
[dev-dependencies]
fraction_index = { version = "^2.0", package = "fractional_index" }

View file

@ -9,6 +9,8 @@ use serde::{Deserialize, Serialize};
mod jitter;
const TERMINATOR: u8 = 128;
static DEFAULT_FRACTIONAL_INDEX: once_cell::sync::Lazy<FractionalIndex> =
once_cell::sync::Lazy::new(|| FractionalIndex(Arc::new(vec![TERMINATOR])));
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
@ -16,7 +18,7 @@ pub struct FractionalIndex(Arc<Vec<u8>>);
impl Default for FractionalIndex {
fn default() -> Self {
FractionalIndex(Arc::new(vec![TERMINATOR]))
DEFAULT_FRACTIONAL_INDEX.clone()
}
}

View file

@ -780,6 +780,7 @@ name = "loro_fractional_index"
version = "0.16.2"
dependencies = [
"imbl",
"once_cell",
"rand",
"serde",
"smallvec",

View file

@ -119,6 +119,9 @@ impl Actor {
}
if let Some(idx) = idx {
if let Container::Tree(tree) = &idx {
tree.set_enable_fractional_index(0);
}
self.add_new_container(idx);
}
}

View file

@ -7,7 +7,7 @@ use loro::{Container, ContainerID, ContainerType, LoroDoc, LoroMap, LoroValue};
use crate::{
actions::{Actionable, FromGenericAction, GenericAction},
actor::{ActionExecutor, ActorTrait},
actor::{assert_value_eq, ActionExecutor, ActorTrait},
crdt_fuzzer::FuzzValue,
value::{ApplyDiff, ContainerTracker, MapTracker, Value},
};
@ -66,7 +66,11 @@ impl ActorTrait for MapActor {
let map = self.loro.get_map("map");
let value_a = map.get_deep_value();
let value_b = self.tracker.lock().unwrap().to_value();
assert_eq!(&value_a, value_b.into_map().unwrap().get("map").unwrap());
assert_value_eq(
&value_a,
value_b.into_map().unwrap().get("map").unwrap(),
None,
);
}
fn container_len(&self) -> u8 {

View file

@ -10,11 +10,10 @@ use loro::{
event::Diff, Container, ContainerID, ContainerType, LoroDoc, LoroError, LoroTree, LoroValue,
TreeExternalDiff, TreeID,
};
use tracing::{debug, trace};
use crate::{
actions::{Actionable, FromGenericAction, GenericAction},
actor::{ActionExecutor, ActorTrait},
actor::{assert_value_eq, ActionExecutor, ActorTrait},
crdt_fuzzer::FuzzValue,
value::{ApplyDiff, ContainerTracker, MapTracker, Value},
};
@ -117,6 +116,7 @@ impl TreeActor {
);
let root = loro.get_tree("tree");
root.set_enable_fractional_index(0);
Self {
loro,
containers: vec![root],
@ -135,7 +135,11 @@ impl ActorTrait for TreeActor {
let tree = loro.get_tree("tree");
let result = tree.get_value_with_meta();
let tracker = self.tracker.lock().unwrap().to_value();
assert_eq!(&result, tracker.into_map().unwrap().get("tree").unwrap());
assert_value_eq(
&result,
tracker.into_map().unwrap().get("tree").unwrap(),
None,
);
}
fn add_new_container(&mut self, container: Container) {
@ -180,7 +184,7 @@ impl Actionable for TreeAction {
}
*parent = (nodes[parent_idx].peer, nodes[parent_idx].counter);
*index %= tree
.children_num(Some(TreeID::new(parent.0, parent.1)))
.children_num(TreeID::new(parent.0, parent.1))
.unwrap_or(0)
+ 1;
}
@ -422,7 +426,7 @@ impl ApplyDiff for TreeTracker {
index,
position,
} => {
self.create_node(target, parent, position.to_string(), index);
self.create_node(target, &parent.tree_id(), position.to_string(), index);
}
TreeExternalDiff::Delete { .. } => {
let node = self.find_node_by_id(target).unwrap();
@ -438,9 +442,10 @@ impl ApplyDiff for TreeTracker {
parent,
index,
position,
..
} => {
let Some(node) = self.find_node_by_id(target) else {
self.create_node(target, parent, position.to_string(), index);
self.create_node(target, &parent.tree_id(), position.to_string(), index);
continue;
};
@ -452,10 +457,10 @@ impl ApplyDiff for TreeTracker {
let index = self.tree.iter().position(|n| n.id == target).unwrap();
self.tree.remove(index)
};
node.parent = *parent;
node.parent = parent.tree_id();
node.position = position.to_string();
if let Some(parent) = parent {
let parent = self.find_node_by_id_mut(*parent).unwrap();
if let Some(parent) = parent.tree_id() {
let parent = self.find_node_by_id_mut(parent).unwrap();
parent.children.insert(*index, node);
} else {
if self.find_node_by_id_mut(target).is_some() {

View file

@ -92,6 +92,8 @@ pub enum LoroTreeError {
TreeNodeNotExist(TreeID),
#[error("The index({index}) should be <= the length of children ({len})")]
IndexOutOfBound { len: usize, index: usize },
#[error("Fractional index is not enabled, you should enable it first by `LoroTree::set_enable_fractional_index`")]
FractionalIndexNotEnabled,
}
#[cfg(feature = "wasm")]

View file

@ -1,4 +1,4 @@
use std::{fmt::Display, io::Write, str::Bytes, sync::Arc};
use std::{fmt::Display, io::Write, sync::Arc};
use arbitrary::Arbitrary;
use enum_as_inner::EnumAsInner;

View file

@ -3,7 +3,7 @@ use criterion::{criterion_group, criterion_main, Criterion};
mod tree {
use super::*;
use criterion::{AxisScale, BenchmarkId, PlotConfiguration};
use loro_internal::LoroDoc;
use loro_internal::{LoroDoc, TreeParentId};
use rand::{rngs::StdRng, Rng};
pub fn tree_move(c: &mut Criterion) {
@ -22,7 +22,7 @@ mod tree {
let loro = LoroDoc::new_auto_commit();
let tree = loro.get_tree("tree");
for idx in 0..*i {
tree.create_at(None, idx as usize).unwrap();
tree.create_at(TreeParentId::Root, idx as usize).unwrap();
}
})
},
@ -39,15 +39,16 @@ mod tree {
let mut ids = vec![];
for _ in 0..SIZE {
let pos = rng.gen::<usize>() % (ids.len() + 1);
ids.push(tree.create_at(None, pos).unwrap());
ids.push(tree.create_at(TreeParentId::Root, pos).unwrap());
}
b.iter(|| {
for _ in 0..*i {
tree.create_at(None, 0).unwrap();
tree.create_at(TreeParentId::Root, 0).unwrap();
let i = rng.gen::<usize>() % SIZE;
let j = rng.gen::<usize>() % SIZE;
tree.mov(ids[i], ids[j]).unwrap_or_default();
tree.mov(ids[i], TreeParentId::Node(ids[j]))
.unwrap_or_default();
}
})
},
@ -62,14 +63,14 @@ mod tree {
let mut versions = vec![];
let size = 1000;
for _ in 0..size {
ids.push(tree.create(None).unwrap())
ids.push(tree.create(TreeParentId::Root).unwrap())
}
let mut rng: StdRng = rand::SeedableRng::seed_from_u64(0);
let mut n = 1000;
while n > 0 {
let i = rng.gen::<usize>() % size;
let j = rng.gen::<usize>() % size;
if tree.mov(ids[i], ids[j]).is_ok() {
if tree.mov(ids[i], TreeParentId::Node(ids[j])).is_ok() {
versions.push(loro.oplog_frontiers());
n -= 1;
};
@ -90,11 +91,13 @@ mod tree {
let tree = loro.get_tree("tree");
let mut ids = vec![];
let mut versions = vec![];
let id1 = tree.create(None).unwrap();
let id1 = tree.create(TreeParentId::Root).unwrap();
ids.push(id1);
versions.push(loro.oplog_frontiers());
for _ in 1..depth {
let id = tree.create(*ids.last().unwrap()).unwrap();
let id = tree
.create(TreeParentId::Node(*ids.last().unwrap()))
.unwrap();
ids.push(id);
versions.push(loro.oplog_frontiers());
}
@ -118,7 +121,7 @@ mod tree {
let mut ids = vec![];
let size = 1000;
for _ in 0..size {
ids.push(tree_a.create(None).unwrap())
ids.push(tree_a.create(TreeParentId::Root).unwrap())
}
doc_b.import(&doc_a.export_snapshot()).unwrap();
let mut rng: StdRng = rand::SeedableRng::seed_from_u64(0);
@ -128,10 +131,14 @@ mod tree {
let i = rng.gen::<usize>() % size;
let j = rng.gen::<usize>() % size;
if t % 2 == 0 {
tree_a.mov(ids[i], ids[j]).unwrap_or_default();
tree_a
.mov(ids[i], TreeParentId::Node(ids[j]))
.unwrap_or_default();
doc_b.import(&doc_a.export_from(&doc_b.oplog_vv())).unwrap();
} else {
tree_b.mov(ids[i], ids[j]).unwrap_or_default();
tree_b
.mov(ids[i], TreeParentId::Node(ids[j]))
.unwrap_or_default();
doc_a.import(&doc_b.export_from(&doc_a.oplog_vv())).unwrap();
}
}

View file

@ -1,6 +1,6 @@
use std::time::Instant;
use loro_internal::LoroDoc;
use loro_internal::{LoroDoc, TreeParentId};
use rand::{rngs::StdRng, Rng};
#[allow(unused)]
@ -10,11 +10,13 @@ fn checkout() {
let tree = loro.get_tree("tree");
let mut ids = vec![];
let mut versions = vec![];
let id1 = tree.create_at(None, 0).unwrap();
let id1 = tree.create_at(TreeParentId::Root, 0).unwrap();
ids.push(id1);
versions.push(loro.oplog_frontiers());
for _ in 1..depth {
let id = tree.create_at(*ids.last().unwrap(), 0).unwrap();
let id = tree
.create_at(TreeParentId::Node(*ids.last().unwrap()), 0)
.unwrap();
ids.push(id);
versions.push(loro.oplog_frontiers());
}
@ -34,7 +36,7 @@ fn mov() {
let mut ids = vec![];
let size = 10000;
for _ in 0..size {
ids.push(tree.create_at(None, 0).unwrap())
ids.push(tree.create_at(TreeParentId::Root, 0).unwrap())
}
let mut rng: StdRng = rand::SeedableRng::seed_from_u64(0);
let n = 100000;
@ -42,8 +44,8 @@ fn mov() {
for _ in 0..n {
let i = rng.gen::<usize>() % size;
let j = rng.gen::<usize>() % size;
let children_num = tree.children_num(Some(ids[j])).unwrap_or(0);
tree.move_to(ids[i], ids[j], children_num)
let children_num = tree.children_num(&TreeParentId::Node(ids[j])).unwrap_or(0);
tree.move_to(ids[i], TreeParentId::Node(ids[j]), children_num)
.unwrap_or_default();
}
println!("encode snapshot size {:?}", loro.export_snapshot().len());
@ -59,7 +61,7 @@ fn create() {
let loro = LoroDoc::default();
let tree = loro.get_tree("tree");
for _ in 0..size {
tree.create_at(None, 0).unwrap();
tree.create_at(TreeParentId::Root, 0).unwrap();
}
println!("encode snapshot size {:?}\n", loro.export_snapshot().len());
println!(

View file

@ -5,8 +5,6 @@ pub struct Configure {
pub(crate) text_style_config: Arc<RwLock<StyleConfigMap>>,
record_timestamp: Arc<AtomicBool>,
pub(crate) merge_interval: Arc<AtomicI64>,
/// do not use `jitter` by default
pub(crate) tree_position_jitter: Arc<AtomicU8>,
}
impl Default for Configure {
@ -15,7 +13,6 @@ impl Default for Configure {
text_style_config: Arc::new(RwLock::new(StyleConfigMap::default_rich_text_config())),
record_timestamp: Arc::new(AtomicBool::new(false)),
merge_interval: Arc::new(AtomicI64::new(1000 * 1000)),
tree_position_jitter: Arc::new(AtomicU8::new(0)),
}
}
}
@ -34,10 +31,6 @@ impl Configure {
self.merge_interval
.load(std::sync::atomic::Ordering::Relaxed),
)),
tree_position_jitter: Arc::new(AtomicU8::new(
self.tree_position_jitter
.load(std::sync::atomic::Ordering::Relaxed),
)),
}
}
@ -55,11 +48,6 @@ impl Configure {
.store(record, std::sync::atomic::Ordering::Relaxed);
}
pub fn set_fractional_index_jitter(&self, jitter: u8) {
self.tree_position_jitter
.store(jitter, std::sync::atomic::Ordering::Relaxed);
}
pub fn merge_interval(&self) -> i64 {
self.merge_interval
.load(std::sync::atomic::Ordering::Relaxed)
@ -77,7 +65,7 @@ pub struct DefaultRandom;
#[cfg(test)]
use std::sync::atomic::AtomicU64;
use std::sync::{
atomic::{AtomicBool, AtomicI64, AtomicU8},
atomic::{AtomicBool, AtomicI64},
Arc, RwLock,
};
#[cfg(test)]

View file

@ -21,16 +21,21 @@ pub struct TreeDiffItem {
#[derive(Debug, Clone, PartialEq)]
pub enum TreeExternalDiff {
Create {
parent: Option<TreeID>,
parent: TreeParentId,
index: usize,
position: FractionalIndex,
},
Move {
parent: Option<TreeID>,
parent: TreeParentId,
index: usize,
position: FractionalIndex,
old_parent: TreeParentId,
old_index: usize,
},
Delete {
old_parent: TreeParentId,
old_index: usize,
},
Delete,
}
impl TreeDiff {

View file

@ -50,7 +50,7 @@ impl DiffCalculatorTrait for TreeDiffCalculator {
fn apply_change(
&mut self,
_oplog: &OpLog,
oplog: &OpLog,
op: crate::op::RichOp,
_vv: Option<&crate::VersionVector>,
) {

View file

@ -11,7 +11,7 @@ use crate::{
diff::{myers_diff, DiffHandler, OperateProxy},
event::{Diff, TextDiffItem},
op::ListSlice,
state::{ContainerState, IndexType, State},
state::{ContainerState, IndexType, State, TreeParentId},
txn::EventHint,
utils::{string_slice::StringSlice, utf16::count_utf16_len},
};
@ -1171,7 +1171,7 @@ impl Handler {
index: _,
position,
} => {
if let Some(p) = parent.as_mut() {
if let TreeParentId::Node(p) = &mut parent {
remap_tree_id(p, container_remap)
}
remap_tree_id(&mut target, container_remap);
@ -1206,14 +1206,16 @@ impl Handler {
mut parent,
index: _,
position,
old_parent: _,
old_index: _,
} => {
if let Some(p) = parent.as_mut() {
if let TreeParentId::Node(p) = &mut parent {
remap_tree_id(p, container_remap)
}
remap_tree_id(&mut target, container_remap);
x.move_at_with_target_for_apply_diff(parent, position, target)?;
}
TreeExternalDiff::Delete => {
TreeExternalDiff::Delete { .. } => {
remap_tree_id(&mut target, container_remap);
if x.contains(target) {
x.delete(target)?;
@ -3939,6 +3941,7 @@ mod test {
use super::{HandlerTrait, TextDelta};
use crate::loro::LoroDoc;
use crate::state::TreeParentId;
use crate::version::Frontiers;
use crate::{fx_map, ToJson};
use loro_common::ID;
@ -4104,7 +4107,7 @@ mod test {
loro.set_peer_id(1).unwrap();
let tree = loro.get_tree("root");
let id = loro
.with_txn(|txn| tree.create_with_txn(txn, None, 0))
.with_txn(|txn| tree.create_with_txn(txn, TreeParentId::Root, 0))
.unwrap();
loro.with_txn(|txn| {
let meta = tree.get_meta(id)?;
@ -4134,11 +4137,11 @@ mod test {
let tree = loro.get_tree("root");
let text = loro.get_text("text");
loro.with_txn(|txn| {
let id = tree.create_with_txn(txn, None, 0)?;
let id = tree.create_with_txn(txn, TreeParentId::Root, 0)?;
let meta = tree.get_meta(id)?;
meta.insert_with_txn(txn, "a", 1.into())?;
text.insert_with_txn(txn, 0, "abc")?;
let _id2 = tree.create_with_txn(txn, None, 0)?;
let _id2 = tree.create_with_txn(txn, TreeParentId::Root, 0)?;
meta.insert_with_txn(txn, "b", 2.into())?;
Ok(id)
})

View file

@ -93,8 +93,8 @@ impl TreeInner {
self.children_links.get(&parent).map(|x| x.len())
}
fn is_parent(&self, target: TreeID, parent: Option<TreeID>) -> bool {
self.parent_links.get(&target) == Some(&parent)
fn is_parent(&self, target: &TreeID, parent: &Option<TreeID>) -> bool {
self.parent_links.get(target) == Some(parent)
}
fn get_index_by_tree_id(&self, target: &TreeID) -> Option<usize> {
@ -164,7 +164,9 @@ impl HandlerTrait for TreeHandler {
let mut q = children
.map(|c| {
VecDeque::from_iter(
c.iter().enumerate().zip(std::iter::repeat(None::<TreeID>)),
c.iter()
.enumerate()
.zip(std::iter::repeat(TreeParentId::Root)),
)
})
.unwrap_or_default();
@ -179,7 +181,7 @@ impl HandlerTrait for TreeHandler {
if let Some(children) = t.value.children_links.get(&Some(*target)) {
for (idx, child) in children.iter().enumerate() {
q.push_back(((idx, child), Some(real_id)));
q.push_back(((idx, child), TreeParentId::Node(real_id)));
}
}
}
@ -210,7 +212,6 @@ impl HandlerTrait for TreeHandler {
self.inner.attached_handler()
}
// TODO:
fn get_value(&self) -> LoroValue {
match &self.inner {
MaybeDetached::Detached(t) => {
@ -291,27 +292,25 @@ impl TreeHandler {
crate::op::RawOpContent::Tree(Arc::new(TreeOp::Delete { target })),
EventHint::Tree(smallvec![TreeDiffItem {
target,
action: TreeExternalDiff::Delete,
action: TreeExternalDiff::Delete {
old_parent: self.get_node_parent(&target).unwrap(),
old_index: self.get_index_by_tree_id(&target).unwrap(),
},
}]),
&inner.state,
)
}
pub fn create<T: Into<Option<TreeID>>>(&self, parent: T) -> LoroResult<TreeID> {
let parent = parent.into();
let index: usize = self.children_num(parent).unwrap_or(0);
pub fn create(&self, parent: TreeParentId) -> LoroResult<TreeID> {
let index: usize = self.children_num(&parent).unwrap_or(0);
self.create_at(parent, index)
}
pub fn create_at<T: Into<Option<TreeID>>>(
&self,
parent: T,
index: usize,
) -> LoroResult<TreeID> {
pub fn create_at(&self, parent: TreeParentId, index: usize) -> LoroResult<TreeID> {
match &self.inner {
MaybeDetached::Detached(t) => {
let t = &mut t.try_lock().unwrap().value;
Ok(t.create(parent.into(), index))
Ok(t.create(parent.tree_id(), index))
}
MaybeDetached::Attached(a) => {
a.with_txn(|txn| self.create_with_txn(txn, parent, index))
@ -322,23 +321,33 @@ impl TreeHandler {
/// For undo/redo, Specify the TreeID of the created node
pub(crate) fn create_at_with_target_for_apply_diff(
&self,
parent: Option<TreeID>,
parent: TreeParentId,
position: FractionalIndex,
target: TreeID,
) -> LoroResult<bool> {
let MaybeDetached::Attached(a) = &self.inner else {
unreachable!();
};
if let Some(p) = self.get_node_parent(&target) {
if p == parent {
return Ok(false);
// If parent is deleted, we need to create the node, so this op from move_apply_diff
} else if !p.is_some_and(|p| !self.contains(p)) {
return self.move_at_with_target_for_apply_diff(parent, position, target);
}
match p {
TreeParentId::Node(p) => {
if self.contains(p) {
return self.move_at_with_target_for_apply_diff(parent, position, target);
}
}
TreeParentId::Root => {
return self.move_at_with_target_for_apply_diff(parent, position, target);
}
TreeParentId::Deleted | TreeParentId::Unexist => {}
}
}
let with_event = !parent.is_some_and(|p| !self.contains(p));
let with_event = !parent.tree_id().is_some_and(|p| !self.contains(p));
if !with_event {
return Ok(false);
}
@ -350,7 +359,7 @@ impl TreeHandler {
let index = self
.get_index_by_fractional_index(
parent,
&parent,
&NodePosition {
position: position.clone(),
idlp: self.next_idlp(),
@ -366,7 +375,7 @@ impl TreeHandler {
inner.container_idx,
crate::op::RawOpContent::Tree(Arc::new(TreeOp::Create {
target,
parent,
parent: parent.tree_id(),
position: position.clone(),
})),
EventHint::Tree(smallvec![TreeDiffItem {
@ -380,11 +389,13 @@ impl TreeHandler {
&inner.state,
)?;
Ok(self.children(Some(target)).unwrap_or_default())
Ok(self
.children(&TreeParentId::Node(target))
.unwrap_or_default())
})?;
for child in children {
let position = self.get_position_by_tree_id(&child).unwrap();
self.create_at_with_target_for_apply_diff(Some(target), position, child)?;
self.create_at_with_target_for_apply_diff(TreeParentId::Node(target), position, child)?;
}
Ok(true)
}
@ -392,7 +403,7 @@ impl TreeHandler {
/// For undo/redo, Specify the TreeID of the created node
pub(crate) fn move_at_with_target_for_apply_diff(
&self,
parent: Option<TreeID>,
parent: TreeParentId,
position: FractionalIndex,
target: TreeID,
) -> LoroResult<bool> {
@ -413,14 +424,14 @@ impl TreeHandler {
let index = self
.get_index_by_fractional_index(
parent,
&parent,
&NodePosition {
position: position.clone(),
idlp: self.next_idlp(),
},
)
.unwrap_or(0);
let with_event = !parent.is_some_and(|p| !self.contains(p));
let with_event = !parent.tree_id().is_some_and(|p| !self.contains(p));
if !with_event {
return Ok(false);
@ -437,7 +448,7 @@ impl TreeHandler {
inner.container_idx,
crate::op::RawOpContent::Tree(Arc::new(TreeOp::Move {
target,
parent,
parent: parent.tree_id(),
position: position.clone(),
})),
EventHint::Tree(smallvec![TreeDiffItem {
@ -446,6 +457,9 @@ impl TreeHandler {
parent,
index,
position: position.clone(),
// the old parent should be exist, so we can unwrap
old_parent: self.get_node_parent(&target).unwrap(),
old_index: self.get_index_by_tree_id(&target).unwrap(),
},
}]),
&inner.state,
@ -454,17 +468,16 @@ impl TreeHandler {
Ok(true)
}
pub(crate) fn create_with_txn<T: Into<Option<TreeID>>>(
pub(crate) fn create_with_txn(
&self,
txn: &mut Transaction,
parent: T,
parent: TreeParentId,
index: usize,
) -> LoroResult<TreeID> {
let inner = self.inner.try_attached_state()?;
let parent: Option<TreeID> = parent.into();
let target = TreeID::from_id(txn.next_id());
match self.generate_position_at(&target, parent, index) {
match self.generate_position_at(&target, &parent, index) {
FractionalIndexGenResult::Ok(position) => {
self.create_with_position(inner, txn, target, parent, index, position)
}
@ -474,26 +487,25 @@ impl TreeHandler {
self.create_with_position(inner, txn, id, parent, index, position)?;
continue;
}
self.mov_with_position(inner, txn, id, parent, index + i, position)?;
self.mov_with_position(inner, txn, id, parent, index + i, position, index + i)?;
}
Ok(target)
}
}
}
pub fn mov<T: Into<Option<TreeID>>>(&self, target: TreeID, parent: T) -> LoroResult<()> {
let parent = parent.into();
pub fn mov(&self, target: TreeID, parent: TreeParentId) -> LoroResult<()> {
match &self.inner {
MaybeDetached::Detached(_) => {
let mut index: usize = self.children_num(parent).unwrap_or(0);
if self.is_parent(target, parent) {
let mut index: usize = self.children_num(&parent).unwrap_or(0);
if self.is_parent(&target, &parent) {
index -= 1;
}
self.move_to(target, parent, index)
}
MaybeDetached::Attached(a) => {
let mut index = self.children_num(parent).unwrap_or(0);
if self.is_parent(target, parent) {
let mut index = self.children_num(&parent).unwrap_or(0);
if self.is_parent(&target, &parent) {
index -= 1;
}
a.with_txn(|txn| self.mov_with_txn(txn, target, parent, index))
@ -502,11 +514,11 @@ impl TreeHandler {
}
pub fn mov_after(&self, target: TreeID, other: TreeID) -> LoroResult<()> {
let parent: Option<TreeID> = self
let parent = self
.get_node_parent(&other)
.ok_or(LoroTreeError::TreeNodeNotExist(other))?;
let mut index = self.get_index_by_tree_id(&other).unwrap() + 1;
if self.is_parent(target, parent) && self.get_index_by_tree_id(&target).unwrap() < index {
if self.is_parent(&target, &parent) && self.get_index_by_tree_id(&target).unwrap() < index {
index -= 1;
}
self.move_to(target, parent, index)
@ -517,7 +529,7 @@ impl TreeHandler {
.get_node_parent(&other)
.ok_or(LoroTreeError::TreeNodeNotExist(other))?;
let mut index = self.get_index_by_tree_id(&other).unwrap();
if self.is_parent(target, parent)
if self.is_parent(&target, &parent)
&& index > 1
&& self.get_index_by_tree_id(&target).unwrap() < index
{
@ -526,16 +538,11 @@ impl TreeHandler {
self.move_to(target, parent, index)
}
pub fn move_to<T: Into<Option<TreeID>>>(
&self,
target: TreeID,
parent: T,
index: usize,
) -> LoroResult<()> {
pub fn move_to(&self, target: TreeID, parent: TreeParentId, index: usize) -> LoroResult<()> {
match &self.inner {
MaybeDetached::Detached(t) => {
let mut t = t.try_lock().unwrap();
t.value.mov(target, parent.into(), index)
t.value.mov(target, parent.tree_id(), index)
}
MaybeDetached::Attached(a) => {
a.with_txn(|txn| self.mov_with_txn(txn, target, parent, index))
@ -543,19 +550,18 @@ impl TreeHandler {
}
}
pub(crate) fn mov_with_txn<T: Into<Option<TreeID>>>(
pub(crate) fn mov_with_txn(
&self,
txn: &mut Transaction,
target: TreeID,
parent: T,
parent: TreeParentId,
index: usize,
) -> LoroResult<()> {
let parent = parent.into();
let inner = self.inner.try_attached_state()?;
let mut children_len = self.children_num(parent).unwrap_or(0);
let mut children_len = self.children_num(&parent).unwrap_or(0);
let mut already_in_parent = false;
// check the input is valid
if self.is_parent(target, parent) {
if self.is_parent(&target, &parent) {
// If the position after moving is same as the current position , do nothing
if let Some(current_index) = self.get_index_by_tree_id(&target) {
if current_index == index {
@ -574,17 +580,18 @@ impl TreeHandler {
}
.into());
}
let old_index = self.get_index_by_tree_id(&target).unwrap();
if already_in_parent {
self.delete_position(parent, target);
self.delete_position(&parent, &target);
}
match self.generate_position_at(&target, parent, index) {
match self.generate_position_at(&target, &parent, index) {
FractionalIndexGenResult::Ok(position) => {
self.mov_with_position(inner, txn, target, parent, index, position)
self.mov_with_position(inner, txn, target, parent, index, position, old_index)
}
FractionalIndexGenResult::Rearrange(ids) => {
for (i, (id, position)) in ids.into_iter().enumerate() {
self.mov_with_position(inner, txn, id, parent, index + i, position)?;
self.mov_with_position(inner, txn, id, parent, index + i, position, old_index)?;
}
Ok(())
}
@ -597,7 +604,7 @@ impl TreeHandler {
inner: &BasicHandler,
txn: &mut Transaction,
tree_id: TreeID,
parent: Option<TreeID>,
parent: TreeParentId,
index: usize,
position: FractionalIndex,
) -> LoroResult<TreeID> {
@ -605,7 +612,7 @@ impl TreeHandler {
inner.container_idx,
crate::op::RawOpContent::Tree(Arc::new(TreeOp::Create {
target: tree_id,
parent,
parent: parent.tree_id(),
position: position.clone(),
})),
EventHint::Tree(smallvec![TreeDiffItem {
@ -627,15 +634,16 @@ impl TreeHandler {
inner: &BasicHandler,
txn: &mut Transaction,
target: TreeID,
parent: Option<TreeID>,
parent: TreeParentId,
index: usize,
position: FractionalIndex,
old_index: usize,
) -> LoroResult<()> {
txn.apply_local_op(
inner.container_idx,
crate::op::RawOpContent::Tree(Arc::new(TreeOp::Move {
target,
parent,
parent: parent.tree_id(),
position: position.clone(),
})),
EventHint::Tree(smallvec![TreeDiffItem {
@ -644,6 +652,8 @@ impl TreeHandler {
parent,
index,
position,
old_parent: self.get_node_parent(&target).unwrap(),
old_index,
},
}]),
&inner.state,
@ -672,46 +682,42 @@ impl TreeHandler {
}
/// Get the parent of the node, if the node is deleted or does not exist, return None
pub fn get_node_parent(&self, target: &TreeID) -> Option<Option<TreeID>> {
pub fn get_node_parent(&self, target: &TreeID) -> Option<TreeParentId> {
match &self.inner {
MaybeDetached::Detached(t) => {
let t = t.try_lock().unwrap();
t.value.get_parent(target)
t.value.get_parent(target).map(TreeParentId::from)
}
MaybeDetached::Attached(a) => a.with_state(|state| {
let a = state.as_tree_state().unwrap();
match a.parent(target) {
TreeParentId::Root => Some(None),
TreeParentId::Node(parent) => Some(Some(parent)),
TreeParentId::Deleted | TreeParentId::Unexist => None,
}
a.parent(target)
}),
}
}
// TODO: iterator
pub fn children(&self, parent: Option<TreeID>) -> Option<Vec<TreeID>> {
pub fn children(&self, parent: &TreeParentId) -> Option<Vec<TreeID>> {
match &self.inner {
MaybeDetached::Detached(t) => {
let t = t.try_lock().unwrap();
t.value.get_children(parent)
t.value.get_children(parent.tree_id())
}
MaybeDetached::Attached(a) => a.with_state(|state| {
let a = state.as_tree_state().unwrap();
a.children(&TreeParentId::from(parent))
a.children(parent)
}),
}
}
pub fn children_num(&self, parent: Option<TreeID>) -> Option<usize> {
pub fn children_num(&self, parent: &TreeParentId) -> Option<usize> {
match &self.inner {
MaybeDetached::Detached(t) => {
let t = t.try_lock().unwrap();
t.value.children_num(parent)
t.value.children_num(parent.tree_id())
}
MaybeDetached::Attached(a) => a.with_state(|state| {
let a = state.as_tree_state().unwrap();
a.children_num(&TreeParentId::from(parent))
a.children_num(parent)
}),
}
}
@ -742,15 +748,15 @@ impl TreeHandler {
}
}
pub fn is_parent(&self, target: TreeID, parent: Option<TreeID>) -> bool {
pub fn is_parent(&self, target: &TreeID, parent: &TreeParentId) -> bool {
match &self.inner {
MaybeDetached::Detached(t) => {
let t = t.try_lock().unwrap();
t.value.is_parent(target, parent)
t.value.is_parent(target, &parent.tree_id())
}
MaybeDetached::Attached(a) => a.with_state(|state| {
let a = state.as_tree_state().unwrap();
a.is_parent(&TreeParentId::from(parent), &target)
a.is_parent(parent, target)
}),
}
}
@ -769,7 +775,7 @@ impl TreeHandler {
}
pub fn roots(&self) -> Vec<TreeID> {
self.children(None).unwrap_or_default()
self.children(&TreeParentId::Root).unwrap_or_default()
}
#[allow(non_snake_case)]
@ -788,7 +794,7 @@ impl TreeHandler {
fn generate_position_at(
&self,
target: &TreeID,
parent: Option<TreeID>,
parent: &TreeParentId,
index: usize,
) -> FractionalIndexGenResult {
let MaybeDetached::Attached(a) = &self.inner else {
@ -796,7 +802,7 @@ impl TreeHandler {
};
a.with_state(|state| {
let a = state.as_tree_state_mut().unwrap();
a.generate_position_at(target, &TreeParentId::from(parent), index)
a.generate_position_at(target, parent, index)
})
}
@ -826,20 +832,20 @@ impl TreeHandler {
}
}
fn delete_position(&self, parent: Option<TreeID>, target: TreeID) {
fn delete_position(&self, parent: &TreeParentId, target: &TreeID) {
let MaybeDetached::Attached(a) = &self.inner else {
unreachable!()
};
a.with_state(|state| {
let a = state.as_tree_state_mut().unwrap();
a.delete_position(&TreeParentId::from(parent), target)
a.delete_position(parent, &target)
})
}
// use for apply diff
pub(crate) fn get_index_by_fractional_index(
&self,
parent: Option<TreeID>,
parent: &TreeParentId,
node_position: &NodePosition,
) -> Option<usize> {
match &self.inner {
@ -848,7 +854,7 @@ impl TreeHandler {
}
MaybeDetached::Attached(a) => a.with_state(|state| {
let a = state.as_tree_state().unwrap();
a.get_index_by_position(&TreeParentId::from(parent), node_position)
a.get_index_by_position(parent, node_position)
}),
}
}
@ -861,4 +867,51 @@ impl TreeHandler {
MaybeDetached::Attached(a) => a.with_txn(|txn| Ok(txn.next_idlp())).unwrap(),
}
}
pub fn is_fractional_index_enabled(&self) -> bool {
match &self.inner {
MaybeDetached::Detached(_) => {
unreachable!()
}
MaybeDetached::Attached(a) => a.with_state(|state| {
let a = state.as_tree_state().unwrap();
a.is_fractional_index_enabled()
}),
}
}
/// Set whether to generate fractional index for Tree Position. The LoroDoc is set to disable fractional index by default.
///
/// The jitter is used to avoid conflicts when multiple users are creating the node at the same position.
/// value 0 is default, which means no jitter, any value larger than 0 will enable jitter.
///
/// Generally speaking, jitter will affect the growth rate of document size.
/// [Read more about it](https://www.loro.dev/blog/movable-tree#implementation-and-encoding-size)
pub fn set_enable_fractional_index(&self, jitter: u8) {
match &self.inner {
MaybeDetached::Detached(_) => {
unreachable!()
}
MaybeDetached::Attached(a) => a.with_state(|state| {
let a = state.as_tree_state_mut().unwrap();
a.enable_generate_fractional_index(jitter);
}),
}
}
/// Disable the fractional index generation for Tree Position when
/// you don't need the Tree's siblings to be sorted. The fractional index will be always default.
///
/// The LoroDoc is set to disable fractional index by default.
pub fn set_disable_fractional_index(&self) {
match &self.inner {
MaybeDetached::Detached(_) => {
unreachable!()
}
MaybeDetached::Attached(a) => a.with_state(|state| {
let a = state.as_tree_state_mut().unwrap();
a.disable_generate_fractional_index();
}),
}
}
}

View file

@ -254,7 +254,7 @@ impl ContainerHistoryCache {
id: node.last_move_op,
op: Arc::new(TreeOp::Create {
target: node.id,
parent: node.parent,
parent: node.parent.tree_id(),
position: node.position.clone(),
}),
effected: true,

View file

@ -21,6 +21,7 @@ pub use loro::LoroDoc;
pub use loro_common;
pub use oplog::OpLog;
pub use state::DocState;
pub use state::TreeParentId;
pub use undo::UndoManager;
pub use utils::subscription::Subscription;
pub mod awareness;

View file

@ -178,16 +178,6 @@ impl LoroDoc {
self.config.set_merge_interval(interval);
}
/// Set the jitter of the tree position(Fractional Index).
///
/// The jitter is used to avoid conflicts when multiple users are creating the node at the same position.
/// value 0 is default, which means no jitter, any value larger than 0 will enable jitter.
/// Generally speaking, jitter will affect the growth rate of document size.
#[inline]
pub fn set_fractional_index_jitter(&self, jitter: u8) {
self.config.set_fractional_index_jitter(jitter);
}
#[inline]
pub fn config_text_style(&self, text_style: StyleConfigMap) {
*self.config.text_style_config.try_write().unwrap() = text_style;

View file

@ -1475,8 +1475,8 @@ impl ChangesBlockBytes {
#[cfg(test)]
mod test {
use crate::{
oplog::convert_change_to_remote, ListHandler, LoroDoc, MovableListHandler, TextHandler,
TreeHandler,
oplog::convert_change_to_remote, state::TreeParentId, ListHandler, LoroDoc,
MovableListHandler, TextHandler, TreeHandler,
};
use super::*;
@ -1554,10 +1554,10 @@ mod test {
let tree = map
.insert_container("tree", TreeHandler::new_detached())
.unwrap();
let node_id = tree.create(None)?;
let node_id = tree.create(TreeParentId::Root)?;
tree.get_meta(node_id)?.insert("key", "value")?;
let node_b = tree.create(None)?;
tree.move_to(node_b, None, 0).unwrap();
let node_b = tree.create(TreeParentId::Root)?;
tree.move_to(node_b, TreeParentId::Root, 0).unwrap();
let movable_list = map
.insert_container("movable", MovableListHandler::new_detached())

View file

@ -1,9 +1,8 @@
use std::{
borrow::Cow,
collections::BTreeMap,
io::Write,
sync::{
atomic::{AtomicU64, AtomicU8, Ordering},
atomic::{AtomicU64, Ordering},
Arc, Mutex, RwLock, Weak,
},
};
@ -15,7 +14,7 @@ use fxhash::{FxHashMap, FxHashSet};
use itertools::Itertools;
use loro_common::{ContainerID, LoroError, LoroResult};
use loro_delta::DeltaItem;
use tracing::{info_span, instrument, trace};
use tracing::{info_span, instrument};
use crate::{
configure::{Configure, DefaultRandom, SecureRandomGenerator},
@ -50,9 +49,8 @@ pub(crate) use container_store::GcStore;
pub(crate) use list_state::ListState;
pub(crate) use map_state::MapState;
pub(crate) use richtext_state::RichtextState;
pub(crate) use tree_state::{
get_meta_value, FractionalIndexGenResult, NodePosition, TreeParentId, TreeState,
};
pub use tree_state::TreeParentId;
pub(crate) use tree_state::{get_meta_value, FractionalIndexGenResult, NodePosition, TreeState};
use self::{container_store::ContainerWrapper, unknown_state::UnknownState};
@ -311,8 +309,8 @@ impl State {
Self::RichtextState(Box::new(RichtextState::new(idx, config)))
}
pub fn new_tree(idx: ContainerIdx, peer: PeerID, jitter: Arc<AtomicU8>) -> Self {
Self::TreeState(Box::new(TreeState::new(idx, peer, jitter)))
pub fn new_tree(idx: ContainerIdx, peer: PeerID) -> Self {
Self::TreeState(Box::new(TreeState::new(idx, peer)))
}
pub fn new_unknown(idx: ContainerIdx) -> Self {
@ -494,7 +492,6 @@ impl DocState {
panic!("apply_diff should not be called in a transaction");
}
trace!("ApplyDiff {:?}", &diff.new_version);
let is_recording = self.is_recording();
self.pre_txn(diff.origin.clone(), diff.by);
let Cow::Owned(mut diffs) = std::mem::take(&mut diff.diff) else {
@ -1487,11 +1484,7 @@ fn create_state_(idx: ContainerIdx, config: &Configure, peer: u64) -> State {
idx,
config.text_style_config.clone(),
))),
ContainerType::Tree => State::TreeState(Box::new(TreeState::new(
idx,
peer,
config.tree_position_jitter.clone(),
))),
ContainerType::Tree => State::TreeState(Box::new(TreeState::new(idx, peer))),
ContainerType::MovableList => State::MovableListState(Box::new(MovableListState::new(idx))),
#[cfg(feature = "counter")]
ContainerType::Counter => {

View file

@ -1,6 +1,4 @@
#[cfg(feature = "counter")]
use super::counter_state::CounterState;
use super::{ContainerCreationContext, MovableListState, State, TreeState};
use super::{ContainerCreationContext, State};
use crate::{
arena::SharedArena,
configure::Configure,
@ -504,7 +502,7 @@ mod encode {
#[cfg(test)]
mod test {
use super::*;
use crate::{ListHandler, LoroDoc, MapHandler, MovableListHandler};
use crate::{state::TreeParentId, ListHandler, LoroDoc, MapHandler, MovableListHandler};
fn decode_container_store(bytes: Bytes) -> ContainerStore {
let mut new_store = ContainerStore::new(
@ -529,8 +527,8 @@ mod test {
list.push("item1").unwrap();
let tree = doc.get_tree("tree");
let root = tree.create(None).unwrap();
tree.create_at(Some(root), 0).unwrap();
let root = tree.create(TreeParentId::Root).unwrap();
tree.create_at(TreeParentId::Node(root), 0).unwrap();
let movable_list = doc.get_movable_list("movable_list");
movable_list.insert(0, "movable_item").unwrap();

View file

@ -18,8 +18,7 @@ use crate::{
handler::ValueOrHandler,
op::{ListSlice, Op, RawOp},
state::movable_list_state::inner::PushElemInfo,
txn::Transaction,
ApplyDiff, DocState, ListDiff,
txn::Transaction, DocState, ListDiff,
};
use self::{

View file

@ -13,7 +13,6 @@ use serde::Serialize;
use std::collections::VecDeque;
use std::fmt::Debug;
use std::ops::{Deref, DerefMut};
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::{Arc, Mutex, Weak};
use super::{ContainerState, DiffApplyContext};
@ -33,6 +32,16 @@ use crate::{
op::RawOp,
};
#[derive(Clone, Debug, Default, EnumAsInner)]
pub enum TreeFractionalIndexConfigInner {
GenerateFractionalIndex {
jitter: u8,
rng: Box<rand::rngs::StdRng>,
},
#[default]
AlwaysDefault,
}
/// The state of movable tree.
///
/// using flat representation
@ -41,8 +50,8 @@ pub struct TreeState {
idx: ContainerIdx,
trees: FxHashMap<TreeID, TreeStateNode>,
children: TreeChildrenCache,
rng: Option<rand::rngs::StdRng>,
jitter: u8,
fractional_index_config: TreeFractionalIndexConfigInner,
peer_id: PeerID,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@ -86,8 +95,28 @@ impl From<Option<TreeID>> for TreeParentId {
}
}
impl From<TreeID> for TreeParentId {
fn from(id: TreeID) -> Self {
if TreeID::is_deleted_root(&id) {
TreeParentId::Deleted
} else {
TreeParentId::Node(id)
}
}
}
impl From<&TreeID> for TreeParentId {
fn from(id: &TreeID) -> Self {
if TreeID::is_deleted_root(id) {
TreeParentId::Deleted
} else {
TreeParentId::Node(*id)
}
}
}
impl TreeParentId {
fn id(&self) -> Option<TreeID> {
pub fn tree_id(&self) -> Option<TreeID> {
match self {
TreeParentId::Node(id) => Some(*id),
TreeParentId::Root => None,
@ -597,16 +626,13 @@ impl NodePosition {
}
impl TreeState {
pub fn new(idx: ContainerIdx, peer_id: PeerID, config: Arc<AtomicU8>) -> Self {
let jitter = config.load(Ordering::Relaxed);
let use_jitter = jitter != 1;
pub fn new(idx: ContainerIdx, peer_id: PeerID) -> Self {
Self {
idx,
trees: FxHashMap::default(),
children: Default::default(),
rng: use_jitter.then_some(rand::rngs::StdRng::seed_from_u64(peer_id)),
jitter,
fractional_index_config: TreeFractionalIndexConfigInner::default(),
peer_id,
}
}
@ -630,7 +656,7 @@ impl TreeState {
}
if let Some(old_parent) = self.trees.get(&target).map(|x| x.parent) {
// remove old position
self.delete_position(&old_parent, target);
self.delete_position(&old_parent, &target);
}
let entry = self.children.entry(parent).or_default();
@ -677,16 +703,9 @@ impl TreeState {
!self.is_node_deleted(&target)
}
pub fn contains_internal(&self, target: &TreeID) -> bool {
self.trees.contains_key(target)
}
/// Get the parent of the node, if the node is deleted or does not exist, return None
pub fn parent(&self, target: &TreeID) -> TreeParentId {
self.trees
.get(target)
.map(|x| x.parent)
.unwrap_or(TreeParentId::Unexist)
pub fn parent(&self, target: &TreeID) -> Option<TreeParentId> {
self.trees.get(target).map(|x| x.parent)
}
/// If the node exists and is not deleted, return false.
@ -716,7 +735,11 @@ impl TreeState {
let children = self.children.get(&root);
let mut q = children
.map(|x| {
VecDeque::from_iter(x.iter().enumerate().zip(std::iter::repeat(None::<TreeID>)))
VecDeque::from_iter(
x.iter()
.enumerate()
.zip(std::iter::repeat(TreeParentId::Root)),
)
})
.unwrap_or_default();
@ -734,7 +757,7 @@ impl TreeState {
.iter()
.enumerate()
.map(|(index, (position, this_target))| {
((index, (position, this_target)), Some(target))
((index, (position, this_target)), TreeParentId::Node(target))
}),
);
}
@ -755,7 +778,7 @@ impl TreeState {
for (index, (position, target)) in children.iter().enumerate() {
ans.push(TreeNode {
id: *target,
parent: root.id(),
parent: root,
position: position.position.clone(),
index,
last_move_op: self.trees.get(target).map(|x| x.last_move_op).unwrap(),
@ -813,9 +836,9 @@ impl TreeState {
}
/// Delete the position cache of the node
pub(crate) fn delete_position(&mut self, parent: &TreeParentId, target: TreeID) {
pub(crate) fn delete_position(&mut self, parent: &TreeParentId, target: &TreeID) {
if let Some(x) = self.children.get_mut(parent) {
x.delete_child(&target);
x.delete_child(target);
}
}
@ -825,25 +848,57 @@ impl TreeState {
parent: &TreeParentId,
index: usize,
) -> FractionalIndexGenResult {
if let Some(rng) = self.rng.as_mut() {
self.children
.entry(*parent)
.or_default()
.generate_fi_at_jitter(index, target, rng, self.jitter)
} else {
self.children
.entry(*parent)
.or_default()
.generate_fi_at(index, target)
match &mut self.fractional_index_config {
TreeFractionalIndexConfigInner::GenerateFractionalIndex { jitter, rng } => {
if *jitter == 0 {
self.children
.entry(*parent)
.or_default()
.generate_fi_at(index, target)
} else {
self.children
.entry(*parent)
.or_default()
.generate_fi_at_jitter(index, target, rng.as_mut(), *jitter)
}
}
TreeFractionalIndexConfigInner::AlwaysDefault => {
FractionalIndexGenResult::Ok(FractionalIndex::default())
}
}
}
pub(crate) fn is_fractional_index_enabled(&self) -> bool {
!matches!(
self.fractional_index_config,
TreeFractionalIndexConfigInner::AlwaysDefault
)
}
pub(crate) fn enable_generate_fractional_index(&mut self, jitter: u8) {
if let TreeFractionalIndexConfigInner::GenerateFractionalIndex {
jitter: old_jitter, ..
} = &mut self.fractional_index_config
{
*old_jitter = jitter;
return;
}
self.fractional_index_config = TreeFractionalIndexConfigInner::GenerateFractionalIndex {
jitter,
rng: Box::new(rand::rngs::StdRng::seed_from_u64(self.peer_id)),
};
}
pub(crate) fn disable_generate_fractional_index(&mut self) {
self.fractional_index_config = TreeFractionalIndexConfigInner::AlwaysDefault;
}
pub(crate) fn get_position(&self, target: &TreeID) -> Option<FractionalIndex> {
self.trees.get(target).and_then(|x| x.position.clone())
}
pub(crate) fn get_index_by_tree_id(&self, target: &TreeID) -> Option<usize> {
let parent = self.parent(target);
let parent = self.parent(target)?;
(!parent.is_deleted())
.then(|| {
self.children
@ -916,13 +971,15 @@ impl ContainerState for TreeState {
ans.push(TreeDiffItem {
target,
action: TreeExternalDiff::Create {
parent: parent.into_node().ok(),
parent: *parent,
index,
position: position.clone(),
},
});
}
TreeInternalDiff::Move { parent, position } => {
let old_parent = self.trees.get(&target).unwrap().parent;
let old_index = self.get_index_by_tree_id(&target).unwrap();
if need_check {
let was_alive = !self.is_node_deleted(&target);
if self
@ -934,32 +991,35 @@ impl ContainerState for TreeState {
// delete event
ans.push(TreeDiffItem {
target,
action: TreeExternalDiff::Delete,
action: TreeExternalDiff::Delete {
old_parent,
old_index,
},
});
}
// Otherwise, it's a normal move inside deleted nodes, no event is needed
} else if was_alive {
// normal move
ans.push(TreeDiffItem {
target,
action: TreeExternalDiff::Move {
parent: *parent,
index: self.get_index_by_tree_id(&target).unwrap(),
position: position.clone(),
old_parent,
old_index,
},
});
} else {
if was_alive {
// normal move
ans.push(TreeDiffItem {
target,
action: TreeExternalDiff::Move {
parent: parent.into_node().ok(),
index: self.get_index_by_tree_id(&target).unwrap(),
position: position.clone(),
},
});
} else {
// create event
ans.push(TreeDiffItem {
target,
action: TreeExternalDiff::Create {
parent: parent.into_node().ok(),
index: self.get_index_by_tree_id(&target).unwrap(),
position: position.clone(),
},
});
}
// create event
ans.push(TreeDiffItem {
target,
action: TreeExternalDiff::Create {
parent: *parent,
index: self.get_index_by_tree_id(&target).unwrap(),
position: position.clone(),
},
});
}
}
} else {
@ -970,9 +1030,11 @@ impl ContainerState for TreeState {
ans.push(TreeDiffItem {
target,
action: TreeExternalDiff::Move {
parent: parent.into_node().ok(),
parent: *parent,
index,
position: position.clone(),
old_parent,
old_index,
},
});
};
@ -982,15 +1044,17 @@ impl ContainerState for TreeState {
if need_check && self.is_node_deleted(&target) {
send_event = false;
}
self.mov(target, *parent, last_move_op, position.clone(), false)
.unwrap();
if send_event {
ans.push(TreeDiffItem {
target,
action: TreeExternalDiff::Delete,
action: TreeExternalDiff::Delete {
old_parent: self.trees.get(&target).unwrap().parent,
old_index: self.get_index_by_tree_id(&target).unwrap(),
},
});
}
self.mov(target, *parent, last_move_op, position.clone(), false)
.unwrap();
}
TreeInternalDiff::MoveInDelete { parent, position } => {
self.mov(target, *parent, last_move_op, position.clone(), false)
@ -1001,7 +1065,10 @@ impl ContainerState for TreeState {
if !self.is_node_deleted(&target) {
ans.push(TreeDiffItem {
target,
action: TreeExternalDiff::Delete,
action: TreeExternalDiff::Delete {
old_parent: self.trees.get(&target).unwrap().parent,
old_index: self.get_index_by_tree_id(&target).unwrap(),
},
});
}
// delete it from state
@ -1037,7 +1104,9 @@ impl ContainerState for TreeState {
// create associated metadata container
match &diff.action {
TreeInternalDiff::Create { parent, position }
| TreeInternalDiff::Move { parent, position } => {
| TreeInternalDiff::Move {
parent, position, ..
} => {
if need_check {
self.mov(target, *parent, last_move_op, Some(position.clone()), true)
.unwrap_or_default();
@ -1126,7 +1195,7 @@ impl ContainerState for TreeState {
let diff = TreeDiffItem {
target: *node,
action: TreeExternalDiff::Create {
parent: node_parent.into_node().ok(),
parent: node_parent,
index,
position: position.position.clone(),
},
@ -1237,7 +1306,7 @@ pub(crate) fn get_meta_value(nodes: &mut Vec<LoroValue>, state: &mut DocState) {
pub(crate) struct TreeNode {
pub(crate) id: TreeID,
pub(crate) parent: Option<TreeID>,
pub(crate) parent: TreeParentId,
pub(crate) position: FractionalIndex,
pub(crate) index: usize,
pub(crate) last_move_op: IdFull,
@ -1249,6 +1318,7 @@ impl TreeNode {
t.insert("id".to_string(), self.id.to_string().into());
let p = self
.parent
.tree_id()
.map(|p| p.to_string().into())
.unwrap_or(LoroValue::Null);
t.insert("parent".to_string(), p);
@ -1428,16 +1498,12 @@ mod snapshot {
let n = state.trees.get(&node.id).unwrap();
let last_set_id = n.last_move_op;
nodes.push(EncodedTreeNode {
parent_idx_plus_two: node
.parent
.map(|p| {
if p.is_deleted_root() {
1
} else {
id_to_idx.get(&p).unwrap() + 2
}
})
.unwrap_or(0),
parent_idx_plus_two: match node.parent {
TreeParentId::Deleted => 1,
TreeParentId::Root => 0,
TreeParentId::Node(id) => id_to_idx.get(&id).unwrap() + 2,
TreeParentId::Unexist => unreachable!(),
},
last_set_peer_idx: peers.register(&last_set_id.peer),
last_set_counter: last_set_id.counter,
last_set_lamport: last_set_id.lamport,
@ -1493,8 +1559,7 @@ mod snapshot {
peers.push(PeerID::from_le_bytes(buf));
}
let mut tree =
TreeState::new(idx, ctx.peer, ctx.configure.tree_position_jitter.clone());
let mut tree = TreeState::new(idx, ctx.peer);
let encoded: EncodedTree = serde_columnar::from_bytes(bytes)?;
let fractional_indexes = PositionArena::decode(&encoded.fractional_indexes).unwrap();
let fractional_indexes = fractional_indexes.parse_to_positions();
@ -1549,10 +1614,10 @@ mod snapshot {
doc.set_peer_id(0).unwrap();
doc.start_auto_commit();
let tree = doc.get_tree("tree");
let a = tree.create(None).unwrap();
let b = tree.create(None).unwrap();
let _c = tree.create(None).unwrap();
tree.mov(b, a).unwrap();
let a = tree.create(TreeParentId::Root).unwrap();
let b = tree.create(TreeParentId::Root).unwrap();
let _c = tree.create(TreeParentId::Root).unwrap();
tree.mov(b, TreeParentId::Node(a)).unwrap();
let (bytes, value) = {
let mut doc_state = doc.app_state().lock().unwrap();
let tree_state = doc_state.get_tree("tree").unwrap();

View file

@ -5,7 +5,7 @@ use std::{
};
use bytes::Bytes;
use loro_kv_store::{compress::CompressionType, mem_store::MemKvConfig, MemKvStore};
use loro_kv_store::{mem_store::MemKvConfig, MemKvStore};
use crate::kv_store::KvStore;

View file

@ -558,8 +558,12 @@ pub mod wasm {
position,
} => {
js_sys::Reflect::set(&obj, &"action".into(), &"create".into()).unwrap();
js_sys::Reflect::set(&obj, &"parent".into(), &JsValue::from(*parent))
.unwrap();
js_sys::Reflect::set(
&obj,
&"parent".into(),
&JsValue::from(parent.tree_id()),
)
.unwrap();
js_sys::Reflect::set(&obj, &"index".into(), &(*index).into()).unwrap();
js_sys::Reflect::set(
&obj,
@ -568,18 +572,34 @@ pub mod wasm {
)
.unwrap();
}
TreeExternalDiff::Delete { .. } => {
TreeExternalDiff::Delete {
old_parent,
old_index,
} => {
js_sys::Reflect::set(&obj, &"action".into(), &"delete".into()).unwrap();
js_sys::Reflect::set(
&obj,
&"old_parent".into(),
&JsValue::from(old_parent.tree_id()),
)
.unwrap();
js_sys::Reflect::set(&obj, &"old_index".into(), &(*old_index).into())
.unwrap();
}
TreeExternalDiff::Move {
parent,
index,
position,
..
old_parent,
old_index,
} => {
js_sys::Reflect::set(&obj, &"action".into(), &"move".into()).unwrap();
js_sys::Reflect::set(&obj, &"parent".into(), &JsValue::from(*parent))
.unwrap();
js_sys::Reflect::set(
&obj,
&"parent".into(),
&JsValue::from(parent.tree_id()),
)
.unwrap();
js_sys::Reflect::set(&obj, &"index".into(), &(*index).into()).unwrap();
js_sys::Reflect::set(
&obj,
@ -587,6 +607,14 @@ pub mod wasm {
&position.to_string().into(),
)
.unwrap();
js_sys::Reflect::set(
&obj,
&"old_parent".into(),
&JsValue::from(old_parent.tree_id()),
)
.unwrap();
js_sys::Reflect::set(&obj, &"old_index".into(), &(*old_index).into())
.unwrap();
}
}
array.push(&obj);

View file

@ -8,6 +8,7 @@ use loro_internal::{
handler::{Handler, TextDelta, ValueOrHandler},
version::Frontiers,
ApplyDiff, HandlerTrait, ListHandler, LoroDoc, MapHandler, TextHandler, ToJson, TreeHandler,
TreeParentId,
};
use serde_json::json;
@ -724,11 +725,11 @@ fn tree_checkout() {
doc_a.subscribe_root(Arc::new(|_e| {}));
doc_a.set_peer_id(1).unwrap();
let tree = doc_a.get_tree("root");
let id1 = tree.create(None).unwrap();
let id2 = tree.create(id1).unwrap();
let id1 = tree.create(TreeParentId::Root).unwrap();
let id2 = tree.create(TreeParentId::Node(id1)).unwrap();
let v1_state = tree.get_deep_value();
let v1 = doc_a.oplog_frontiers();
let _id3 = tree.create(id2).unwrap();
let _id3 = tree.create(TreeParentId::Node(id2)).unwrap();
let v2_state = tree.get_deep_value();
let v2 = doc_a.oplog_frontiers();
tree.delete(id2).unwrap();
@ -757,7 +758,7 @@ fn tree_checkout() {
);
doc_a.attach();
tree.create(None).unwrap();
tree.create(TreeParentId::Root).unwrap();
}
#[test]
@ -853,8 +854,8 @@ fn missing_event_when_checkout() {
let doc2 = LoroDoc::new_auto_commit();
let tree = doc2.get_tree("tree");
let node = tree.create_at(None, 0).unwrap();
let _ = tree.create_at(None, 0).unwrap();
let node = tree.create_at(TreeParentId::Root, 0).unwrap();
let _ = tree.create_at(TreeParentId::Root, 0).unwrap();
let meta = tree.get_meta(node).unwrap();
meta.insert("a", 0).unwrap();
doc.import(&doc2.export_from(&doc.oplog_vv())).unwrap();
@ -930,7 +931,7 @@ fn insert_attach_container() -> LoroResult<()> {
#[test]
fn tree_attach() {
let tree = TreeHandler::new_detached();
let id = tree.create(None).unwrap();
let id = tree.create(TreeParentId::Root).unwrap();
tree.get_meta(id).unwrap().insert("key", "value").unwrap();
let doc = LoroDoc::new_auto_commit();
doc.get_list("list").insert_container(0, tree).unwrap();

View file

@ -0,0 +1,25 @@
use loro_internal::{LoroDoc, TreeParentId};
#[test]
fn tree_index() {
let doc = LoroDoc::new_auto_commit();
doc.set_peer_id(0).unwrap();
let tree = doc.get_tree("tree");
let root = tree.create(TreeParentId::Root).unwrap();
let child = tree.create(root.into()).unwrap();
let child2 = tree.create_at(root.into(), 0).unwrap();
// sort with OpID
assert_eq!(tree.get_index_by_tree_id(&child).unwrap(), 0);
assert_eq!(tree.get_index_by_tree_id(&child2).unwrap(), 1);
let doc = LoroDoc::new_auto_commit();
doc.set_peer_id(0).unwrap();
let tree = doc.get_tree("tree");
tree.set_enable_fractional_index(0);
let root = tree.create(TreeParentId::Root).unwrap();
let child = tree.create(root.into()).unwrap();
let child2 = tree.create_at(root.into(), 0).unwrap();
// sort with fractional index
assert_eq!(tree.get_index_by_tree_id(&child).unwrap(), 1);
assert_eq!(tree.get_index_by_tree_id(&child2).unwrap(), 0);
}

File diff suppressed because it is too large Load diff

View file

@ -13,12 +13,14 @@ use loro_internal::encoding::ImportBlobMetadata;
use loro_internal::handler::HandlerTrait;
use loro_internal::handler::ValueOrHandler;
use loro_internal::json::JsonChange;
use loro_internal::loro_common::LoroTreeError;
use loro_internal::obs::LocalUpdateCallback;
use loro_internal::undo::{OnPop, OnPush};
use loro_internal::version::ImVersionVector;
use loro_internal::DocState;
use loro_internal::LoroDoc as InnerLoroDoc;
use loro_internal::OpLog;
use loro_internal::TreeParentId;
use loro_internal::{
handler::Handler as InnerHandler, ListHandler as InnerListHandler,
MapHandler as InnerMapHandler, MovableListHandler as InnerMovableListHandler,
@ -160,16 +162,6 @@ impl LoroDoc {
self.doc.set_change_merge_interval(interval);
}
/// Set the jitter of the tree position(Fractional Index).
///
/// The jitter is used to avoid conflicts when multiple users are creating the node at the same position.
/// value 0 is default, which means no jitter, any value larger than 0 will enable jitter.
/// Generally speaking, jitter will affect the growth rate of document size.
#[inline]
pub fn set_fractional_index_jitter(&self, jitter: u8) {
self.doc.set_fractional_index_jitter(jitter);
}
/// Set the rich text format configuration of the document.
///
/// You need to config it if you use rich text `mark` method.
@ -1465,10 +1457,8 @@ impl LoroTree {
/// // create a new child
/// let child = tree.create(root).unwrap();
/// ```
pub fn create<T: Into<Option<TreeID>>>(&self, parent: T) -> LoroResult<TreeID> {
let parent = parent.into();
let index = self.children_num(parent).unwrap_or(0);
self.handler.create_at(parent, index)
pub fn create<T: Into<TreeParentId>>(&self, parent: T) -> LoroResult<TreeID> {
self.handler.create(parent.into())
}
/// Get the root nodes of the forest.
@ -1488,17 +1478,18 @@ impl LoroTree {
///
/// let doc = LoroDoc::new();
/// let tree = doc.get_tree("tree");
/// // enable generate fractional index
/// tree.set_enable_fractional_index(0);
/// // create a root
/// let root = tree.create(None).unwrap();
/// // create a new child at index 0
/// let child = tree.create_at(root, 0).unwrap();
/// ```
pub fn create_at<T: Into<Option<TreeID>>>(
&self,
parent: T,
index: usize,
) -> LoroResult<TreeID> {
self.handler.create_at(parent, index)
pub fn create_at<T: Into<TreeParentId>>(&self, parent: T, index: usize) -> LoroResult<TreeID> {
if !self.handler.is_fractional_index_enabled() {
return Err(LoroTreeError::FractionalIndexNotEnabled.into());
}
self.handler.create_at(parent.into(), index)
}
/// Move the `target` node to be a child of the `parent` node.
@ -1517,10 +1508,8 @@ impl LoroTree {
/// // move `root2` to be a child of `root`.
/// tree.mov(root2, root).unwrap();
/// ```
pub fn mov<T: Into<Option<TreeID>>>(&self, target: TreeID, parent: T) -> LoroResult<()> {
let parent = parent.into();
let index = self.children_num(parent).unwrap_or(0);
self.handler.move_to(target, parent, index)
pub fn mov<T: Into<TreeParentId>>(&self, target: TreeID, parent: T) -> LoroResult<()> {
self.handler.mov(target, parent.into())
}
/// Move the `target` node to be a child of the `parent` node at the given index.
@ -1533,19 +1522,23 @@ impl LoroTree {
///
/// let doc = LoroDoc::new();
/// let tree = doc.get_tree("tree");
/// // enable generate fractional index
/// tree.set_enable_fractional_index(0);
/// let root = tree.create(None).unwrap();
/// let root2 = tree.create(None).unwrap();
/// // move `root2` to be a child of `root` at index 0.
/// tree.mov_to(root2, root, 0).unwrap();
/// ```
pub fn mov_to<T: Into<Option<TreeID>>>(
pub fn mov_to<T: Into<TreeParentId>>(
&self,
target: TreeID,
parent: T,
to: usize,
) -> LoroResult<()> {
let parent = parent.into();
self.handler.move_to(target, parent, to)
if !self.handler.is_fractional_index_enabled() {
return Err(LoroTreeError::FractionalIndexNotEnabled.into());
}
self.handler.move_to(target, parent.into(), to)
}
/// Move the `target` node to be a child after the `after` node with the same parent.
@ -1557,12 +1550,17 @@ impl LoroTree {
///
/// let doc = LoroDoc::new();
/// let tree = doc.get_tree("tree");
/// // enable generate fractional index
/// tree.set_enable_fractional_index(0);
/// let root = tree.create(None).unwrap();
/// let root2 = tree.create(None).unwrap();
/// // move `root` to be a child after `root2`.
/// tree.mov_after(root, root2).unwrap();
/// ```
pub fn mov_after(&self, target: TreeID, after: TreeID) -> LoroResult<()> {
if !self.handler.is_fractional_index_enabled() {
return Err(LoroTreeError::FractionalIndexNotEnabled.into());
}
self.handler.mov_after(target, after)
}
@ -1575,12 +1573,17 @@ impl LoroTree {
///
/// let doc = LoroDoc::new();
/// let tree = doc.get_tree("tree");
/// // enable generate fractional index
/// tree.set_enable_fractional_index(0);
/// let root = tree.create(None).unwrap();
/// let root2 = tree.create(None).unwrap();
/// // move `root` to be a child before `root2`.
/// tree.mov_before(root, root2).unwrap();
/// ```
pub fn mov_before(&self, target: TreeID, before: TreeID) -> LoroResult<()> {
if !self.handler.is_fractional_index_enabled() {
return Err(LoroTreeError::FractionalIndexNotEnabled.into());
}
self.handler.mov_before(target, before)
}
@ -1625,7 +1628,7 @@ impl LoroTree {
///
/// - If the target node does not exist, return `None`.
/// - If the target node is a root node, return `Some(None)`.
pub fn parent(&self, target: TreeID) -> Option<Option<TreeID>> {
pub fn parent(&self, target: TreeID) -> Option<TreeParentId> {
self.handler.get_node_parent(&target)
}
@ -1642,13 +1645,14 @@ impl LoroTree {
/// Return all children of the target node.
///
/// If the parent node does not exist, return `None`.
pub fn children(&self, parent: Option<TreeID>) -> Option<Vec<TreeID>> {
self.handler.children(parent)
pub fn children<T: Into<TreeParentId>>(&self, parent: T) -> Option<Vec<TreeID>> {
self.handler.children(&parent.into())
}
/// Return the number of children of the target node.
pub fn children_num(&self, parent: Option<TreeID>) -> Option<usize> {
self.handler.children_num(parent)
pub fn children_num<T: Into<TreeParentId>>(&self, parent: T) -> Option<usize> {
let parent: TreeParentId = parent.into();
self.handler.children_num(&parent)
}
/// Return container id of the tree.
@ -1682,6 +1686,30 @@ impl LoroTree {
pub fn __internal__next_tree_id(&self) -> TreeID {
self.handler.__internal__next_tree_id()
}
/// Whether the fractional index is enabled.
pub fn is_fractional_index_enabled(&self) -> bool {
self.handler.is_fractional_index_enabled()
}
/// Set whether to generate fractional index for Tree Position.
///
/// The jitter is used to avoid conflicts when multiple users are creating the node at the same position.
/// value 0 is default, which means no jitter, any value larger than 0 will enable jitter.
///
/// Generally speaking, jitter will affect the growth rate of document size.
/// [Read more about it](https://www.loro.dev/blog/movable-tree#implementation-and-encoding-size)
#[inline]
pub fn set_enable_fractional_index(&self, jitter: u8) {
self.handler.set_enable_fractional_index(jitter);
}
/// Disable the fractional index generation for Tree Position when
/// you don't need the Tree's siblings to be sorted. The fractional index will be always default.
#[inline]
pub fn set_disable_fractional_index(&self) {
self.handler.set_disable_fractional_index();
}
}
impl Default for LoroTree {

View file

@ -3,7 +3,7 @@ import {
Container,
ContainerID,
Delta,
Loro,
LoroDoc,
LoroList,
LoroMap,
LoroText,
@ -13,6 +13,11 @@ import {
TreeID,
Value,
} from "loro-wasm";
/**
* @deprecated Please use LoroDoc
*/
export class Loro extends LoroDoc {}
export { Awareness } from "./awareness";
export type Frontiers = OpId[];
@ -88,15 +93,17 @@ export type TreeDiffItem =
action: "create";
parent: TreeID | undefined;
index: number;
position: string;
fractional_index: string;
}
| { target: TreeID; action: "delete" }
| { target: TreeID; action: "delete"; old_parent: TreeID | undefined; old_index: number }
| {
target: TreeID;
action: "move";
parent: TreeID | undefined;
index: number;
position: string;
fractional_index: string;
old_parent: TreeID | undefined;
old_index: number;
};
export type TreeDiff = {
@ -121,14 +128,14 @@ export function isContainerId(s: string): s is ContainerID {
return s.startsWith("cid:");
}
export { Loro };
export { LoroDoc };
/** Whether the value is a container.
*
* # Example
*
* ```ts
* const doc = new Loro();
* const doc = new LoroDoc();
* const map = doc.getMap("map");
* const list = doc.getList("list");
* const text = doc.getText("text");
@ -157,7 +164,7 @@ export function isContainer(value: any): value is Container {
* # Example
*
* ```ts
* const doc = new Loro();
* const doc = new LoroDoc();
* const map = doc.getMap("map");
* const list = doc.getList("list");
* const text = doc.getText("text");
@ -189,7 +196,7 @@ export function getType<T>(
}
declare module "loro-wasm" {
interface Loro {
interface LoroDoc {
subscribe(listener: Listener): number;
}
@ -210,7 +217,7 @@ declare module "loro-wasm" {
setOnPop(listener?: UndoConfig["onPop"]): void;
}
interface Loro<
interface LoroDoc<
T extends Record<string, Container> = Record<string, Container>,
> {
/**
@ -221,9 +228,9 @@ declare module "loro-wasm" {
*
* @example
* ```ts
* import { Loro } from "loro-crdt";
* import { LoroDoc } from "loro-crdt";
*
* const doc = new Loro();
* const doc = new LoroDoc();
* const map = doc.getMap("map");
* ```
*/
@ -238,9 +245,9 @@ declare module "loro-wasm" {
*
* @example
* ```ts
* import { Loro } from "loro-crdt";
* import { LoroDoc } from "loro-crdt";
*
* const doc = new Loro();
* const doc = new LoroDoc();
* const list = doc.getList("list");
* ```
*/
@ -255,9 +262,9 @@ declare module "loro-wasm" {
*
* @example
* ```ts
* import { Loro } from "loro-crdt";
* import { LoroDoc } from "loro-crdt";
*
* const doc = new Loro();
* const doc = new LoroDoc();
* const list = doc.getList("list");
* ```
*/
@ -272,9 +279,9 @@ declare module "loro-wasm" {
*
* @example
* ```ts
* import { Loro } from "loro-crdt";
* import { LoroDoc } from "loro-crdt";
*
* const doc = new Loro();
* const doc = new LoroDoc();
* const tree = doc.getTree("tree");
* ```
*/
@ -292,9 +299,9 @@ declare module "loro-wasm" {
*
* @example
* ```ts
* import { Loro } from "loro-crdt";
* import { LoroDoc } from "loro-crdt";
*
* const doc = new Loro();
* const doc = new LoroDoc();
* const list = doc.getList("list");
* list.insert(0, 100);
* list.insert(1, "foo");
@ -309,9 +316,9 @@ declare module "loro-wasm" {
*
* @example
* ```ts
* import { Loro, LoroText } from "loro-crdt";
* import { LoroDoc, LoroText } from "loro-crdt";
*
* const doc = new Loro();
* const doc = new LoroDoc();
* const list = doc.getList("list");
* list.insert(0, 100);
* const text = list.insertContainer(1, new LoroText());
@ -328,9 +335,9 @@ declare module "loro-wasm" {
*
* @example
* ```ts
* import { Loro } from "loro-crdt";
* import { LoroDoc } from "loro-crdt";
*
* const doc = new Loro();
* const doc = new LoroDoc();
* const list = doc.getList("list");
* list.insert(0, 100);
* console.log(list.get(0)); // 100
@ -343,9 +350,9 @@ declare module "loro-wasm" {
*
* @example
* ```ts
* import { Loro } from "loro-crdt";
* import { LoroDoc } from "loro-crdt";
*
* const doc = new Loro();
* const doc = new LoroDoc();
* const list = doc.getList("list");
* list.insert(0, 100);
* list.insert(1, "foo");
@ -368,9 +375,9 @@ declare module "loro-wasm" {
*
* @example
* ```ts
* import { Loro, LoroText } from "loro-crdt";
* import { LoroDoc, LoroText } from "loro-crdt";
*
* const doc = new Loro();
* const doc = new LoroDoc();
* const list = doc.getMovableList("list");
* list.insert(0, 100);
* list.insert(1, "foo");
@ -385,9 +392,9 @@ declare module "loro-wasm" {
*
* @example
* ```ts
* import { Loro } from "loro-crdt";
* import { LoroDoc } from "loro-crdt";
*
* const doc = new Loro();
* const doc = new LoroDoc();
* const list = doc.getMovableList("list");
* list.insert(0, 100);
* const text = list.insertContainer(1, new LoroText());
@ -404,10 +411,10 @@ declare module "loro-wasm" {
*
* @example
* ```ts
* import { Loro } from "loro-crdt";
* import { LoroDoc } from "loro-crdt";
*
* const doc = new Loro();
* const list = doc.getMoableList("list");
* const doc = new LoroDoc();
* const list = doc.getMovableList("list");
* list.insert(0, 100);
* console.log(list.get(0)); // 100
* console.log(list.get(1)); // undefined
@ -419,9 +426,9 @@ declare module "loro-wasm" {
*
* @example
* ```ts
* import { Loro } from "loro-crdt";
* import { LoroDoc } from "loro-crdt";
*
* const doc = new Loro();
* const doc = new LoroDoc();
* const list = doc.getMovableList("list");
* list.insert(0, 100);
* list.insert(1, "foo");
@ -447,9 +454,9 @@ declare module "loro-wasm" {
*
* @example
* ```ts
* import { Loro } from "loro-crdt";
* import { LoroDoc } from "loro-crdt";
*
* const doc = new Loro();
* const doc = new LoroDoc();
* const list = doc.getList("list");
* list.insert(0, 100);
* list.insert(1, "foo");
@ -464,9 +471,9 @@ declare module "loro-wasm" {
*
* @example
* ```ts
* import { Loro } from "loro-crdt";
* import { LoroDoc } from "loro-crdt";
*
* const doc = new Loro();
* const doc = new LoroDoc();
* const list = doc.getMovableList("list");
* list.insert(0, 100);
* const text = list.setContainer(0, new LoroText());
@ -492,9 +499,9 @@ declare module "loro-wasm" {
*
* @example
* ```ts
* import { Loro } from "loro-crdt";
* import { LoroDoc } from "loro-crdt";
*
* const doc = new Loro();
* const doc = new LoroDoc();
* const map = doc.getMap("map");
* map.set("foo", "bar");
* const bar = map.get("foo");
@ -506,9 +513,9 @@ declare module "loro-wasm" {
*
* @example
* ```ts
* import { Loro } from "loro-crdt";
* import { LoroDoc } from "loro-crdt";
*
* const doc = new Loro();
* const doc = new LoroDoc();
* const map = doc.getMap("map");
* map.set("foo", "bar");
* const text = map.setContainer("text", new LoroText());
@ -528,9 +535,9 @@ declare module "loro-wasm" {
*
* @example
* ```ts
* import { Loro } from "loro-crdt";
* import { LoroDoc } from "loro-crdt";
*
* const doc = new Loro();
* const doc = new LoroDoc();
* const map = doc.getMap("map");
* map.set("foo", "bar");
* const bar = map.get("foo");
@ -544,9 +551,9 @@ declare module "loro-wasm" {
*
* @example
* ```ts
* import { Loro } from "loro-crdt";
* import { LoroDoc } from "loro-crdt";
*
* const doc = new Loro();
* const doc = new LoroDoc();
* const map = doc.getMap("map");
* map.set("foo", "bar");
* map.set("foo", "baz");
@ -579,9 +586,9 @@ declare module "loro-wasm" {
*
* @example
* ```ts
* import { Loro } from "loro-crdt";
* import { LoroDoc } from "loro-crdt";
*
* const doc = new Loro();
* const doc = new LoroDoc();
* const tree = doc.getTree("tree");
* const root = tree.createNode();
* const node = tree.createNode(undefined, 0);
@ -617,9 +624,9 @@ declare module "loro-wasm" {
*
* @example
* ```typescript
* import { Loro } from "loro-crdt";
* import { LoroDoc } from "loro-crdt";
*
* let doc = new Loro();
* let doc = new LoroDoc();
* let tree = doc.getTree("tree");
* let root = tree.createNode();
* let node = root.createNode();

View file

@ -3,7 +3,7 @@ import {
Container,
getType,
isContainer,
Loro,
LoroDoc,
LoroList,
LoroMap,
LoroText,
@ -11,7 +11,7 @@ import {
} from "../src";
it("basic example", () => {
const doc = new Loro();
const doc = new LoroDoc();
const list = doc.getList("list");
list.insert(0, "A");
list.insert(1, "B");
@ -53,7 +53,7 @@ it("basic example", () => {
});
it("get or create on Map", () => {
const docA = new Loro();
const docA = new LoroDoc();
const map = docA.getMap("map");
const container = map.getOrCreateContainer("list", new LoroList());
container.insert(0, 1);
@ -66,8 +66,8 @@ it("get or create on Map", () => {
});
it("basic sync example", () => {
const docA = new Loro();
const docB = new Loro();
const docA = new LoroDoc();
const docB = new LoroDoc();
const listA = docA.getList("list");
listA.insert(0, "A");
listA.insert(1, "B");
@ -91,14 +91,14 @@ it("basic sync example", () => {
});
it("basic events", () => {
const doc = new Loro();
const doc = new LoroDoc();
doc.subscribe((event) => {});
const list = doc.getList("list");
});
describe("list", () => {
it("insert containers", () => {
const doc = new Loro();
const doc = new LoroDoc();
const list = doc.getList("list");
const map = list.insertContainer(0, new LoroMap());
map.set("key", "value");
@ -108,7 +108,7 @@ describe("list", () => {
});
it("toArray", () => {
const doc = new Loro();
const doc = new LoroDoc();
const list = doc.getList("list");
list.insert(0, 1);
list.insert(1, 2);
@ -123,7 +123,7 @@ describe("list", () => {
describe("map", () => {
it("get child container", () => {
const doc = new Loro();
const doc = new LoroDoc();
const map = doc.getMap("map");
const list = map.setContainer("key", new LoroList());
list.insert(0, 1);
@ -132,7 +132,7 @@ describe("map", () => {
});
it("set large int", () => {
const doc = new Loro();
const doc = new LoroDoc();
const map = doc.getMap("map");
map.set("key", 2147483699);
expect(map.get("key")).toBe(2147483699);
@ -141,12 +141,12 @@ describe("map", () => {
describe("import", () => {
it("pending", () => {
const a = new Loro();
const a = new LoroDoc();
a.getText("text").insert(0, "a");
const b = new Loro();
const b = new LoroDoc();
b.import(a.exportFrom());
b.getText("text").insert(1, "b");
const c = new Loro();
const c = new LoroDoc();
c.import(b.exportFrom());
c.getText("text").insert(2, "c");
@ -161,9 +161,9 @@ describe("import", () => {
});
it("import by frontiers", () => {
const a = new Loro();
const a = new LoroDoc();
a.getText("text").insert(0, "a");
const b = new Loro();
const b = new LoroDoc();
b.import(a.exportFrom());
b.getText("text").insert(1, "b");
b.getList("list").insert(0, [1, 2]);
@ -173,18 +173,18 @@ describe("import", () => {
});
it("from snapshot", () => {
const a = new Loro();
const a = new LoroDoc();
a.getText("text").insert(0, "hello");
const bytes = a.exportSnapshot();
const b = Loro.fromSnapshot(bytes);
const b = LoroDoc.fromSnapshot(bytes);
b.getText("text").insert(0, "123");
expect(b.toJSON()).toStrictEqual({ text: "123hello" });
});
it("importBatch Error #181", () => {
const docA = new Loro();
const docA = new LoroDoc();
const updateA = docA.exportSnapshot();
const docB = new Loro();
const docB = new LoroDoc();
docB.importUpdateBatch([updateA]);
docB.getText("text").insert(0, "hello");
docB.commit();
@ -193,7 +193,7 @@ describe("import", () => {
describe("map", () => {
it("keys", () => {
const doc = new Loro();
const doc = new LoroDoc();
const map = doc.getMap("map");
map.set("foo", "bar");
map.set("baz", "bar");
@ -202,7 +202,7 @@ describe("map", () => {
});
it("values", () => {
const doc = new Loro();
const doc = new LoroDoc();
const map = doc.getMap("map");
map.set("foo", "bar");
map.set("baz", "bar");
@ -211,7 +211,7 @@ describe("map", () => {
});
it("entries", () => {
const doc = new Loro();
const doc = new LoroDoc();
const map = doc.getMap("map");
map.set("foo", "bar");
map.set("baz", "bar");
@ -225,7 +225,7 @@ describe("map", () => {
});
it("entries should return container handlers", () => {
const doc = new Loro();
const doc = new LoroDoc();
const map = doc.getMap("map");
map.setContainer("text", new LoroText());
map.set("foo", "bar");
@ -235,7 +235,7 @@ describe("map", () => {
});
it("handlers should still be usable after doc is dropped", () => {
const doc = new Loro();
const doc = new LoroDoc();
const text = doc.getText("text");
const list = doc.getList("list");
const map = doc.getMap("map");
@ -249,9 +249,9 @@ it("handlers should still be usable after doc is dropped", () => {
});
it("get change with given lamport", () => {
const doc1 = new Loro();
const doc1 = new LoroDoc();
doc1.setPeerId(1);
const doc2 = new Loro();
const doc2 = new LoroDoc();
doc2.setPeerId(2);
doc1.getText("text").insert(0, "01234");
doc2.import(doc1.exportFrom());
@ -304,7 +304,7 @@ it("isContainer", () => {
expect(isContainer({})).toBeFalsy();
expect(isContainer(undefined)).toBeFalsy();
expect(isContainer(null)).toBeFalsy();
const doc = new Loro();
const doc = new LoroDoc();
const t = doc.getText("t");
expect(isContainer(t)).toBeTruthy();
expect(isContainer(doc.getMap("m"))).toBeTruthy();
@ -318,7 +318,7 @@ it("isContainer", () => {
it("getValueType", () => {
// Type tests
const doc = new Loro();
const doc = new LoroDoc();
const t = doc.getText("t");
expectTypeOf(getType(t)).toEqualTypeOf<"Text">();
expect(getType(t)).toBe("Text");
@ -346,7 +346,7 @@ it("getValueType", () => {
});
it("enable timestamp", () => {
const doc = new Loro();
const doc = new LoroDoc();
doc.setPeerId(1);
doc.getText("123").insert(0, "123");
doc.commit();
@ -365,7 +365,7 @@ it("enable timestamp", () => {
});
it("commit with specified timestamp", () => {
const doc = new Loro();
const doc = new LoroDoc();
doc.setPeerId(1);
doc.getText("123").insert(0, "123");
doc.commit(undefined, 111);
@ -375,7 +375,7 @@ it("commit with specified timestamp", () => {
it("can control the mergeable interval", () => {
{
const doc = new Loro();
const doc = new LoroDoc();
doc.setPeerId(1);
doc.getText("123").insert(0, "1");
doc.commit(undefined, 110);
@ -385,7 +385,7 @@ it("can control the mergeable interval", () => {
}
{
const doc = new Loro();
const doc = new LoroDoc();
doc.setPeerId(1);
doc.setChangeMergeInterval(10);
doc.getText("123").insert(0, "1");
@ -398,7 +398,7 @@ it("can control the mergeable interval", () => {
});
it("get container parent", () => {
const doc = new Loro();
const doc = new LoroDoc();
const m = doc.getMap("m");
expect(m.parent()).toBeUndefined();
const list = m.setContainer("t", new LoroList());
@ -429,7 +429,7 @@ it("prelim support", () => {
text.delete(1, 1);
expect(list.toJSON()).toStrictEqual([{ "3": 2, "9": 9, text: "Hello" }]);
}
const doc = new Loro();
const doc = new LoroDoc();
const rootMap = doc.getMap("map");
rootMap.setContainer("test", map); // new way to create sub-container
@ -452,7 +452,7 @@ it("prelim support", () => {
});
it("get elem by path", () => {
const doc = new Loro();
const doc = new LoroDoc();
const map = doc.getMap("map");
map.set("key", 1);
expect(doc.getByPath("map/key")).toBe(1);
@ -463,7 +463,7 @@ it("get elem by path", () => {
});
it("fork", () => {
const doc = new Loro();
const doc = new LoroDoc();
const map = doc.getMap("map");
map.set("key", 1);
const doc2 = doc.fork();

View file

@ -1,9 +1,9 @@
import { describe, expect, it } from "vitest";
import { Loro } from "../src";
import { LoroDoc } from "../src";
describe("Checkout", () => {
it("simple checkout", async () => {
const doc = new Loro();
const doc = new LoroDoc();
doc.setPeerId(0n);
const text = doc.getText("text");
text.insert(0, "H");
@ -31,7 +31,7 @@ describe("Checkout", () => {
});
it("Chinese char", () => {
const doc = new Loro();
const doc = new LoroDoc();
const text = doc.getText("text");
text.insert(0, "你好世界");
doc.commit();
@ -55,13 +55,13 @@ describe("Checkout", () => {
});
it("two clients", () => {
const doc = new Loro();
const doc = new LoroDoc();
const text = doc.getText("text");
text.insert(0, "0");
doc.commit();
const v0 = doc.frontiers();
const docB = new Loro();
const docB = new LoroDoc();
docB.import(doc.exportFrom());
expect(docB.cmpWithFrontiers(v0)).toBe(0);
text.insert(1, "0");

View file

@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { CounterDiff, Loro } from "../src";
import { CounterDiff, LoroDoc } from "../src";
function oneMs(): Promise<void> {
return new Promise((r) => setTimeout(r));
@ -7,7 +7,7 @@ function oneMs(): Promise<void> {
describe("counter", () => {
it("increment", () => {
const doc = new Loro();
const doc = new LoroDoc();
const counter = doc.getCounter("counter");
counter.increment(1);
counter.increment(2);
@ -16,7 +16,7 @@ describe("counter", () => {
});
it("encode", async () => {
const doc = new Loro();
const doc = new LoroDoc();
const counter = doc.getCounter("counter");
counter.increment(1);
counter.increment(2);
@ -25,13 +25,13 @@ describe("counter", () => {
const updates = doc.exportFrom();
const snapshot = doc.exportSnapshot();
const json = doc.exportJsonUpdates();
const doc2 = new Loro();
const doc2 = new LoroDoc();
doc2.import(updates);
expect(doc2.toJSON()).toStrictEqual(doc.toJSON());
const doc3 = new Loro();
const doc3 = new LoroDoc();
doc3.import(snapshot);
expect(doc3.toJSON()).toStrictEqual(doc.toJSON());
const doc4 = new Loro();
const doc4 = new LoroDoc();
doc4.importJsonUpdates(json);
expect(doc4.toJSON()).toStrictEqual(doc.toJSON());
});
@ -39,7 +39,7 @@ describe("counter", () => {
describe("counter event", () => {
it("event", async () => {
const doc = new Loro();
const doc = new LoroDoc();
let triggered = false;
doc.subscribe((e) => {
triggered = true;

View file

@ -3,7 +3,7 @@ import {
Delta,
getType,
ListDiff,
Loro,
LoroDoc,
LoroEventBatch,
LoroList,
LoroMap,
@ -14,7 +14,7 @@ import {
describe("event", () => {
it("target", async () => {
const loro = new Loro();
const loro = new LoroDoc();
let lastEvent: undefined | LoroEventBatch;
loro.subscribe((event) => {
expect(event.by).toBe("local");
@ -29,7 +29,7 @@ describe("event", () => {
});
it("path", async () => {
const loro = new Loro();
const loro = new LoroDoc();
let lastEvent: undefined | LoroEventBatch;
loro.subscribe((event) => {
lastEvent = event;
@ -52,7 +52,7 @@ describe("event", () => {
});
it("text diff", async () => {
const loro = new Loro();
const loro = new LoroDoc();
let lastEvent: undefined | LoroEventBatch;
loro.subscribe((event) => {
lastEvent = event;
@ -75,7 +75,7 @@ describe("event", () => {
});
it("list diff", async () => {
const loro = new Loro();
const loro = new LoroDoc();
let lastEvent: undefined | LoroEventBatch;
loro.subscribe((event) => {
lastEvent = event;
@ -98,7 +98,7 @@ describe("event", () => {
});
it("map diff", async () => {
const loro = new Loro();
const loro = new LoroDoc();
let lastEvent: undefined | LoroEventBatch;
loro.subscribe((event) => {
lastEvent = event;
@ -129,7 +129,7 @@ describe("event", () => {
});
it("tree", async () => {
const loro = new Loro();
const loro = new LoroDoc();
let lastEvent: undefined | LoroEventBatch;
loro.subscribe((event) => {
lastEvent = event;
@ -144,7 +144,7 @@ describe("event", () => {
describe("subscribe container events", () => {
it("text", async () => {
const loro = new Loro();
const loro = new LoroDoc();
const text = loro.getText("text");
let ran = 0;
const sub = text.subscribe((event) => {
@ -179,7 +179,7 @@ describe("event", () => {
});
it("map subscribe deep", async () => {
const loro = new Loro();
const loro = new LoroDoc();
const map = loro.getMap("map");
let times = 0;
const sub = map.subscribe((event) => {
@ -208,7 +208,7 @@ describe("event", () => {
});
it("list subscribe deep", async () => {
const loro = new Loro();
const loro = new LoroDoc();
const list = loro.getList("list");
let times = 0;
const sub = list.subscribe((event) => {
@ -236,7 +236,7 @@ describe("event", () => {
describe("text event length should be utf16", () => {
it("test", async () => {
const loro = new Loro();
const loro = new LoroDoc();
const text = loro.getText("text");
let string = "";
text.subscribe((event) => {
@ -285,7 +285,7 @@ describe("event", () => {
describe("handler in event", () => {
it("test", async () => {
const loro = new Loro();
const loro = new LoroDoc();
const list = loro.getList("list");
let first = true;
loro.subscribe((e) => {
@ -304,7 +304,7 @@ describe("event", () => {
});
it("diff can contain containers", async () => {
const doc = new Loro();
const doc = new LoroDoc();
const list = doc.getList("list");
let ran = false;
doc.subscribe((event) => {
@ -330,11 +330,11 @@ describe("event", () => {
});
it("remote event", async () => {
const doc = new Loro();
const doc = new LoroDoc();
const list = doc.getList("list");
list.insert(0, 123);
{
const doc2 = new Loro();
const doc2 = new LoroDoc();
let triggered = false;
doc2.subscribe((event) => {
expect(event.by).toBe("import");
@ -345,7 +345,7 @@ describe("event", () => {
expect(triggered).toBeTruthy();
}
{
const doc2 = new Loro();
const doc2 = new LoroDoc();
let triggered = false;
doc2.subscribe((event) => {
expect(event.by).toBe("import");
@ -358,7 +358,7 @@ describe("event", () => {
});
it("checkout event", async () => {
const doc = new Loro();
const doc = new LoroDoc();
const list = doc.getList("list");
list.insert(0, 123);
doc.commit();

View file

@ -1,14 +1,14 @@
import { describe, expect, expectTypeOf, it } from "vitest";
import { Loro } from "../src";
import { LoroDoc } from "../src";
import { Container, LoroText, OpId } from "../src";
import { setDebug } from "loro-wasm";
it("#211", () => {
const loro1 = new Loro();
const loro1 = new LoroDoc();
loro1.setPeerId(0n);
const text1 = loro1.getText("text");
const loro2 = new Loro();
const loro2 = new LoroDoc();
loro2.setPeerId(1n);
const text2 = loro2.getText("text");
@ -59,7 +59,7 @@ it("#211", () => {
show(text1, loro1, text2, loro2);
});
function show(text1: LoroText, loro1: Loro, text2: LoroText, loro2: Loro) {
function show(text1: LoroText, loro1: LoroDoc, text2: LoroText, loro2: LoroDoc) {
// console.log(` #0 has content: ${JSON.stringify(text1.toString())}`);
// console.log(` #0 has frontiers: ${showFrontiers(loro1.frontiers())}`);
// console.log(` #1 has content: ${JSON.stringify(text2.toString())}`);

View file

@ -1,12 +1,12 @@
import { expect, it } from "vitest";
import {
Loro,
LoroDoc,
LoroMap,
TextOp,
} from "../src";
it("json encoding", () => {
const doc = new Loro();
const doc = new LoroDoc();
const text = doc.getText("text");
text.insert(0, "123");
const map = doc.getMap("map");
@ -26,7 +26,7 @@ it("json encoding", () => {
text.mark({ start: 0, end: 3 }, "bold", true);
const json = doc.exportJsonUpdates();
// console.log(json.changes[0].ops);
const doc2 = new Loro();
const doc2 = new LoroDoc();
doc2.importJsonUpdates(json);
});
@ -134,13 +134,13 @@ it("json decoding", () => {
}
]
}`;
const doc = new Loro();
const doc = new LoroDoc();
doc.importJsonUpdates(v15Json);
// console.log(doc.exportJsonUpdates());
});
it("test some type correctness", () => {
const doc = new Loro();
const doc = new LoroDoc();
doc.setPeerId(0);
doc.getText("text").insert(0, "123");
doc.commit();

View file

@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { Loro, LoroList, LoroMap, LoroText, VersionVector } from "../src";
import { LoroDoc, LoroList, LoroMap, LoroText, VersionVector } from "../src";
import { expectTypeOf } from "vitest";
function assertEquals(a: any, b: any) {
@ -8,7 +8,7 @@ function assertEquals(a: any, b: any) {
describe("transaction", () => {
it("transaction", async () => {
const loro = new Loro();
const loro = new LoroDoc();
const text = loro.getText("text");
let count = 0;
const sub = loro.subscribe(() => {
@ -26,7 +26,7 @@ describe("transaction", () => {
});
it("transaction origin", async () => {
const loro = new Loro();
const loro = new LoroDoc();
const text = loro.getText("text");
let count = 0;
const sub = loro.subscribe((event: { origin: string }) => {
@ -48,7 +48,7 @@ describe("transaction", () => {
describe("subscribe", () => {
it("subscribe_lock", async () => {
const loro = new Loro();
const loro = new LoroDoc();
const text = loro.getText("text");
const list = loro.getList("list");
let count = 0;
@ -80,7 +80,7 @@ describe("subscribe", () => {
});
it("subscribe_lock2", async () => {
const loro = new Loro();
const loro = new LoroDoc();
const text = loro.getText("text");
let count = 0;
const sub = loro.subscribe(() => {
@ -101,7 +101,7 @@ describe("subscribe", () => {
});
it("subscribe", async () => {
const loro = new Loro();
const loro = new LoroDoc();
const text = loro.getText("text");
let count = 0;
const sub = loro.subscribe(() => {
@ -125,8 +125,8 @@ describe("subscribe", () => {
describe("sync", () => {
it("two insert at beginning", async () => {
const a = new Loro();
const b = new Loro();
const a = new LoroDoc();
const b = new LoroDoc();
let a_version: undefined | VersionVector = undefined;
let b_version: undefined | VersionVector = undefined;
a.subscribe((e) => {
@ -153,11 +153,11 @@ describe("sync", () => {
});
it("sync", () => {
const loro = new Loro();
const loro = new LoroDoc();
const text = loro.getText("text");
text.insert(0, "hello world");
const loro_bk = new Loro();
const loro_bk = new LoroDoc();
loro_bk.import(loro.exportFrom(undefined));
assertEquals(loro_bk.toJSON(), loro.toJSON());
const text_bk = loro_bk.getText("text");
@ -172,7 +172,7 @@ describe("sync", () => {
});
describe("wasm", () => {
const loro = new Loro();
const loro = new LoroDoc();
const a = loro.getText("ha");
a.insert(0, "hello world");
a.delete(6, 5);
@ -208,14 +208,14 @@ describe("wasm", () => {
describe("type", () => {
it("test map type", () => {
const loro = new Loro<{ map: LoroMap<{ name: "he" }> }>();
const loro = new LoroDoc<{ map: LoroMap<{ name: "he" }> }>();
const map = loro.getMap("map");
const v = map.get("name");
expectTypeOf(v).toEqualTypeOf<"he">();
});
it("test recursive map type", () => {
const loro = new Loro<{ map: LoroMap<{ map: LoroMap<{ name: "he" }> }> }>();
const loro = new LoroDoc<{ map: LoroMap<{ map: LoroMap<{ name: "he" }> }> }>();
const map = loro.getMap("map");
map.setContainer("map", new LoroMap());
@ -225,7 +225,7 @@ describe("type", () => {
});
it("works for list type", () => {
const loro = new Loro<{ list: LoroList<string> }>();
const loro = new LoroDoc<{ list: LoroList<string> }>();
const list = loro.getList("list");
list.insert(0, "123");
const v0 = list.get(0);
@ -233,7 +233,7 @@ describe("type", () => {
});
it("test binary type", () => {
const loro = new Loro<{ list: LoroList<Uint8Array> }>();
const loro = new LoroDoc<{ list: LoroList<Uint8Array> }>();
const list = loro.getList("list");
list.insert(0, new Uint8Array(10));
const v0 = list.get(0);
@ -245,7 +245,7 @@ describe("type", () => {
describe("list stable position", () => {
it("basic tests", () => {
const loro = new Loro();
const loro = new LoroDoc();
const list = loro.getList("list");
list.insert(0, "a");
const pos0 = list.getCursor(0);

View file

@ -2,7 +2,7 @@ import { describe, expect, expectTypeOf, it } from "vitest";
import {
Delta,
ListDiff,
Loro,
LoroDoc,
LoroList,
LoroMap,
LoroMovableList,
@ -12,7 +12,7 @@ import {
describe("movable list", () => {
it("should work like list", () => {
const doc = new Loro();
const doc = new LoroDoc();
const list = doc.getMovableList("list");
expect(list.length).toBe(0);
list.push("a");
@ -24,7 +24,7 @@ describe("movable list", () => {
});
it("can be synced", () => {
const doc = new Loro();
const doc = new LoroDoc();
const list = doc.getMovableList("list");
list.push("a");
list.push("b");
@ -32,7 +32,7 @@ describe("movable list", () => {
expect(list.toArray()).toEqual(["a", "b", "c"]);
list.set(2, "d");
list.move(0, 1);
const doc2 = new Loro();
const doc2 = new LoroDoc();
const list2 = doc2.getMovableList("list");
expect(list2.length).toBe(0);
doc2.import(doc.exportFrom());
@ -43,7 +43,7 @@ describe("movable list", () => {
});
it("should support move", () => {
const doc = new Loro();
const doc = new LoroDoc();
const list = doc.getMovableList("list");
list.push("a");
list.push("b");
@ -54,7 +54,7 @@ describe("movable list", () => {
});
it("should support set", () => {
const doc = new Loro();
const doc = new LoroDoc();
const list = doc.getMovableList("list");
list.push("a");
list.push("b");
@ -65,7 +65,7 @@ describe("movable list", () => {
});
it.todo("should support get cursor", () => {
const doc = new Loro();
const doc = new LoroDoc();
doc.setPeerId(1);
const list = doc.getMovableList("list");
list.push("a");
@ -88,7 +88,7 @@ describe("movable list", () => {
});
it("inserts sub-container", () => {
const doc = new Loro();
const doc = new LoroDoc();
const list = doc.getMovableList("list");
list.push("a");
list.push("b");
@ -105,7 +105,7 @@ describe("movable list", () => {
});
it("can be inserted into a list as an attached container", () => {
const doc = new Loro();
const doc = new LoroDoc();
const list = doc.getMovableList("list");
list.push("a");
list.push("b");
@ -121,12 +121,12 @@ describe("movable list", () => {
});
it("length should be correct when there are concurrent move", () => {
const docA = new Loro();
const docA = new LoroDoc();
const list = docA.getMovableList("list");
list.push("a");
list.push("b");
list.push("c");
const docB = new Loro();
const docB = new LoroDoc();
const listB = docB.getMovableList("list");
docB.import(docA.exportFrom());
listB.move(0, 1);
@ -137,13 +137,13 @@ describe("movable list", () => {
});
it("concurrent set the one with larger peer id win", () => {
const docA = new Loro();
const docA = new LoroDoc();
docA.setPeerId(0);
const listA = docA.getMovableList("list");
listA.push("a");
listA.push("b");
listA.push("c");
const docB = new Loro();
const docB = new LoroDoc();
docB.setPeerId(1);
const listB = docB.getMovableList("list");
docB.import(docA.exportFrom());
@ -158,7 +158,7 @@ describe("movable list", () => {
});
it("can be subscribe", async () => {
const doc = new Loro();
const doc = new LoroDoc();
const list = doc.getMovableList("list");
list.push("a");
list.push("b");
@ -198,7 +198,7 @@ describe("movable list", () => {
});
it("has the right type", () => {
const doc = new Loro<
const doc = new LoroDoc<
{ list: LoroMovableList<LoroMap<{ name: string }>> }
>();
const list = doc.getMovableList("list");
@ -209,7 +209,7 @@ describe("movable list", () => {
});
it("set container", () => {
const doc = new Loro();
const doc = new LoroDoc();
const list = doc.getMovableList("list");
list.insert(0, 100);
const text = list.setContainer(0, new LoroText());

View file

@ -1,10 +1,10 @@
import { describe, expect, it } from "vitest";
import { Delta, Loro, TextDiff } from "../src";
import { Delta, LoroDoc, TextDiff } from "../src";
import { Cursor, OpId, PeerID, setDebug } from "loro-wasm";
describe("richtext", () => {
it("mark", () => {
const doc = new Loro();
const doc = new LoroDoc();
doc.configTextStyle({
bold: { expand: "after" },
link: { expand: "before" },
@ -26,7 +26,7 @@ describe("richtext", () => {
});
it("insert after emoji", () => {
const doc = new Loro();
const doc = new LoroDoc();
const text = doc.getText("text");
text.insert(0, "👨‍👩‍👦");
text.insert(8, "a");
@ -34,7 +34,7 @@ describe("richtext", () => {
});
it("emit event correctly", async () => {
const doc = new Loro();
const doc = new LoroDoc();
const text = doc.getText("text");
let triggered = false;
text.subscribe((e) => {
@ -63,7 +63,7 @@ describe("richtext", () => {
});
it("emit event from merging doc correctly", async () => {
const doc = new Loro();
const doc = new LoroDoc();
const text = doc.getText("text");
let called = false;
text.subscribe((event) => {
@ -83,7 +83,7 @@ describe("richtext", () => {
}
});
const docB = new Loro();
const docB = new LoroDoc();
const textB = docB.getText("text");
textB.insert(0, "Hello World!");
textB.mark({ start: 0, end: 5 }, "bold", true);
@ -93,7 +93,7 @@ describe("richtext", () => {
});
it("Delete emoji", async () => {
const doc = new Loro();
const doc = new LoroDoc();
const text = doc.getText("text");
text.insert(0, "012345👨👩👦6789");
doc.commit();
@ -116,13 +116,13 @@ describe("richtext", () => {
});
it("apply delta", async () => {
const doc1 = new Loro();
const doc1 = new LoroDoc();
doc1.configTextStyle({
link: { expand: "none" },
bold: { expand: "after" },
});
const text1 = doc1.getText("text");
const doc2 = new Loro();
const doc2 = new LoroDoc();
doc2.configTextStyle({
link: { expand: "none" },
bold: { expand: "after" },
@ -154,7 +154,7 @@ describe("richtext", () => {
});
it("custom richtext type", async () => {
const doc = new Loro();
const doc = new LoroDoc();
doc.configTextStyle({
myStyle: {
expand: "none",
@ -178,7 +178,7 @@ describe("richtext", () => {
});
it("allow overlapped styles", () => {
const doc = new Loro();
const doc = new LoroDoc();
doc.configTextStyle({
comment: { expand: "none" },
});
@ -206,7 +206,7 @@ describe("richtext", () => {
});
it("Cursor example", () => {
const doc = new Loro();
const doc = new LoroDoc();
const text = doc.getText("text");
text.insert(0, "123");
const pos0 = text.getCursor(0, 0);
@ -222,7 +222,7 @@ describe("richtext", () => {
});
it("Get and query cursor", () => {
const doc = new Loro();
const doc = new LoroDoc();
const text = doc.getText("text");
doc.setPeerId("1");
text.insert(0, "123");
@ -243,7 +243,7 @@ describe("richtext", () => {
const bytes = pos0!.encode();
// Sending pos0 over the network
const pos0decoded = Cursor.decode(bytes);
const docA = new Loro();
const docA = new LoroDoc();
docA.import(doc.exportFrom());
{
const ans = docA.getCursorPos(pos0decoded!);
@ -272,7 +272,7 @@ describe("richtext", () => {
});
it("Styles should not affect cursor pos", () => {
const doc = new Loro();
const doc = new LoroDoc();
const text = doc.getText("text");
text.insert(0, "Hello");
const pos3 = text.getCursor(3);
@ -282,13 +282,13 @@ describe("richtext", () => {
});
it("Insert cursed str", () => {
const doc = new Loro();
const doc = new LoroDoc();
const text = doc.getText("text");
text.insert(0, `“aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`);
});
it("Insert/delete by utf8 index", () => {
const doc = new Loro();
const doc = new LoroDoc();
const text = doc.getText('t');
text.insert(0, "你好");
text.insertUtf8(3, "a");
@ -303,28 +303,28 @@ describe("richtext", () => {
});
it("Slice", () => {
const doc = new Loro();
const doc = new LoroDoc();
const text = doc.getText('t');
text.insert(0, "你好");
expect(text.slice(0, 1)).toStrictEqual("你");
});
it("Slice emoji", () => {
const doc = new Loro();
const doc = new LoroDoc();
const text = doc.getText('t');
text.insert(0, "😡😡😡");
expect(text.slice(0, 2)).toStrictEqual("😡");
});
it("CharAt", () => {
const doc = new Loro();
const doc = new LoroDoc();
const text = doc.getText('t');
text.insert(0, "你好");
expect(text.charAt(1)).toStrictEqual("好");
});
it("Splice", () => {
const doc = new Loro();
const doc = new LoroDoc();
const text = doc.getText('t');
text.insert(0, "你好");
expect(text.splice(1, 1, "我")).toStrictEqual("好");
@ -332,7 +332,7 @@ describe("richtext", () => {
});
it("Text iter", () => {
const doc = new Loro();
const doc = new LoroDoc();
const text = doc.getText('t');
text.insert(0, "你好");
let str = "";
@ -344,7 +344,7 @@ describe("richtext", () => {
});
it("Text update", () => {
const doc = new Loro();
const doc = new LoroDoc();
const text = doc.getText('t');
text.insert(0, "Hello 😊Bro");
text.update("Hello World Bro😊");

View file

@ -1,13 +1,14 @@
import { assert, describe, expect, it} from "vitest";
import { Loro, LoroTree, LoroTreeNode } from "../src";
import { LoroDoc, LoroTree, LoroTreeNode, TreeDiff } from "../src";
function assertEquals(a: any, b: any) {
expect(a).toStrictEqual(b);
}
describe("loro tree", () => {
const loro = new Loro();
const loro = new LoroDoc();
const tree = loro.getTree("root");
tree.setEnableFractionalIndex(0);
it("create", () => {
const root = tree.createNode();
@ -76,7 +77,7 @@ describe("loro tree", () => {
});
it("toArray", ()=>{
const loro2 = new Loro();
const loro2 = new LoroDoc();
const tree2 = loro2.getTree("root");
const root = tree2.createNode();
tree2.createNode(root.id);
@ -119,8 +120,9 @@ describe("loro tree", () => {
});
describe("loro tree node", ()=>{
const loro = new Loro();
const loro = new LoroDoc();
const tree = loro.getTree("root");
tree.setEnableFractionalIndex(0);
it("create", () => {
const root = tree.createNode();
@ -180,8 +182,28 @@ describe("loro tree node", ()=>{
assertEquals(child.index(), 1);
assertEquals(child2.index(), 0);
});
it("old parent", () => {
const root = tree.createNode();
const child = root.createNode();
const child2 = root.createNode();
loro.commit();
const subID = tree.subscribe((e)=>{
if(e.events[0].diff.type == "tree"){
const diff = e.events[0].diff as TreeDiff;
if (diff.diff[0].action == "move"){
assertEquals(diff.diff[0].old_parent, root.id);
assertEquals(diff.diff[0].old_index, 1);
}
}
});
child2.move(child);
loro.commit();
tree.unsubscribe(subID);
assertEquals(child2.parent()!.id, child.id);
});
});
function one_ms(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, 1));
}
}

View file

@ -1,5 +1,5 @@
import {
Loro,
LoroDoc,
LoroList,
LoroMap,
LoroMovableList,
@ -16,7 +16,7 @@ test("Container should not match Value", () => {
});
test("A non-numeric string is not a valid peer id", () => {
const doc = new Loro();
const doc = new LoroDoc();
expectTypeOf(doc.peerIdStr).toMatchTypeOf<PeerID>();
expectTypeOf("123" as const).toMatchTypeOf<PeerID>();
expectTypeOf("a123" as const).not.toMatchTypeOf<PeerID>();
@ -34,7 +34,7 @@ test("Expect container type", () => {
});
test("doc type and container type", () => {
const doc = new Loro<{
const doc = new LoroDoc<{
text: LoroText;
map: LoroMap<{
name?: string;

View file

@ -1,9 +1,9 @@
import { Cursor, Loro, UndoManager } from "../src";
import { Cursor, LoroDoc, UndoManager } from "../src";
import { describe, expect, test } from "vitest";
describe("undo", () => {
test("basic text undo", () => {
const doc = new Loro();
const doc = new LoroDoc();
doc.setPeerId(1);
const undo = new UndoManager(doc, { maxUndoSteps: 100, mergeInterval: 0 });
expect(undo.canRedo()).toBeFalsy();
@ -41,7 +41,7 @@ describe("undo", () => {
});
test("merge", async () => {
const doc = new Loro();
const doc = new LoroDoc();
const undo = new UndoManager(doc, { maxUndoSteps: 100, mergeInterval: 50 });
for (let i = 0; i < 10; i++) {
doc.getText("text").insert(i, i.toString());
@ -66,7 +66,7 @@ describe("undo", () => {
});
test("max undo steps", () => {
const doc = new Loro();
const doc = new LoroDoc();
const undo = new UndoManager(doc, { maxUndoSteps: 100, mergeInterval: 0 });
for (let i = 0; i < 200; i++) {
doc.getText("text").insert(0, "0");
@ -80,7 +80,7 @@ describe("undo", () => {
});
test("Skip chosen events", () => {
const doc = new Loro();
const doc = new LoroDoc();
const undo = new UndoManager(doc, {
maxUndoSteps: 100,
mergeInterval: 0,
@ -130,7 +130,7 @@ describe("undo", () => {
});
test("undo event's origin", async () => {
const doc = new Loro();
const doc = new LoroDoc();
let undoing = false;
let ran = false;
doc.subscribe((e) => {
@ -151,7 +151,7 @@ describe("undo", () => {
});
test("undo event listener", async () => {
const doc = new Loro();
const doc = new LoroDoc();
let pushReturn: null | number = null;
let expectedValue: null | number = null;
@ -199,7 +199,7 @@ describe("undo", () => {
});
test("undo cursor transform", async () => {
const doc = new Loro();
const doc = new LoroDoc();
let cursors: Cursor[] = [];
let poppedCursors: Cursor[] = [];
const undo = new UndoManager(doc , {

View file

@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
import {
decodeImportBlobMeta,
Loro,
LoroDoc,
LoroMap,
OpId,
VersionVector,
@ -9,14 +9,14 @@ import {
describe("Frontiers", () => {
it("two clients", () => {
const doc = new Loro();
const doc = new LoroDoc();
doc.setPeerId(0);
const text = doc.getText("text");
text.insert(0, "0");
doc.commit();
const v0 = doc.frontiers();
const docB = new Loro();
const docB = new LoroDoc();
docB.setPeerId(1);
docB.import(doc.exportFrom());
expect(docB.cmpWithFrontiers(v0)).toBe(0);
@ -34,9 +34,9 @@ describe("Frontiers", () => {
});
it("cmp frontiers", () => {
const doc1 = new Loro();
const doc1 = new LoroDoc();
doc1.setPeerId(1);
const doc2 = new Loro();
const doc2 = new LoroDoc();
doc2.setPeerId(2n);
doc1.getText("text").insert(0, "01234");
@ -85,7 +85,7 @@ describe("Frontiers", () => {
});
it("peer id repr should be consistent", () => {
const doc = new Loro();
const doc = new LoroDoc();
const id = doc.peerIdStr;
doc.getText("text").insert(0, "hello");
doc.commit();
@ -105,9 +105,9 @@ it("peer id repr should be consistent", () => {
});
describe("Version", () => {
const a = new Loro();
const a = new LoroDoc();
a.setPeerId(0n);
const b = new Loro();
const b = new LoroDoc();
b.setPeerId(1n);
a.getText("text").insert(0, "ha");
b.getText("text").insert(0, "yo");
@ -155,7 +155,7 @@ describe("Version", () => {
});
it("get import blob metadata", () => {
const doc0 = new Loro();
const doc0 = new LoroDoc();
doc0.setPeerId(0n);
const text = doc0.getText("text");
text.insert(0, "0");
@ -172,7 +172,7 @@ it("get import blob metadata", () => {
expect(meta.startFrontiers.length).toBe(0);
}
const doc1 = new Loro();
const doc1 = new LoroDoc();
doc1.setPeerId(1);
doc1.getText("text").insert(0, "123");
doc1.import(doc0.exportFrom());