From 9ecc0a90b11a43de5e83cf4b763fa321a3507c42 Mon Sep 17 00:00:00 2001 From: Zixuan Chen Date: Sat, 30 Mar 2024 11:38:24 +0800 Subject: [PATCH] refactor!: Add prelim support, making creating sub container easier (#300) This PR includes a BREAKING CHANGE. It enables you to create containers before attaching them to the document, making the API more intuitive and straightforward. A container can be either attached to a document or detached. When it's detached, its history/state does not persist. You can attach a container to a document by inserting it into an attached container. Once a container is attached, its state, along with all of its descendants's states, will be recreated in the document. After attaching, the container and its descendants, will each have their corresponding "attached" version of themselves? When a detached container x is attached to a document, you can use x.getAttached() to obtain the corresponding attached container. --- crates/examples/src/draw.rs | 57 +- crates/examples/src/sheet.rs | 4 +- crates/fuzz/src/container/list.rs | 2 +- crates/fuzz/src/container/map.rs | 2 +- crates/fuzz/src/container/tree.rs | 2 +- crates/loro-common/src/error.rs | 6 + crates/loro-internal/benches/event.rs | 6 +- crates/loro-internal/examples/event.rs | 26 +- .../src/container/richtext/richtext_state.rs | 4 +- crates/loro-internal/src/fuzz.rs | 3 +- .../src/fuzz/recursive_refactored.rs | 16 +- crates/loro-internal/src/handler.rs | 1807 +++++++++++++---- crates/loro-internal/src/loro.rs | 16 +- crates/loro-internal/src/oplog/dag.rs | 8 +- crates/loro-internal/src/state.rs | 1 + crates/loro-internal/src/txn.rs | 13 +- crates/loro-internal/tests/autocommit.rs | 6 +- crates/loro-internal/tests/test.rs | 38 +- crates/loro-wasm/src/convert.rs | 95 +- crates/loro-wasm/src/lib.rs | 278 ++- crates/loro/README.md | 16 +- crates/loro/src/lib.rs | 331 ++- crates/loro/tests/loro_rust_test.rs | 73 +- crates/loro/tests/readme.rs | 47 + loro-js/src/index.ts | 35 +- loro-js/tests/basic.test.ts | 68 +- loro-js/tests/event.test.ts | 20 +- loro-js/tests/misc.test.ts | 15 +- loro-js/tests/version.test.ts | 54 +- loro-js/tsconfig.json | 10 +- 30 files changed, 2326 insertions(+), 733 deletions(-) create mode 100644 crates/loro/tests/readme.rs diff --git a/crates/examples/src/draw.rs b/crates/examples/src/draw.rs index bc9dd5ca..0f57a33b 100644 --- a/crates/examples/src/draw.rs +++ b/crates/examples/src/draw.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use bench_utils::{draw::DrawAction, Action}; -use loro::{ContainerID, ContainerType}; +use loro::{ContainerID, LoroList, LoroMap, LoroText}; use crate::{run_actions_fuzz_in_async_mode, ActorTrait}; @@ -41,26 +41,13 @@ impl ActorTrait for DrawActor { fn apply_action(&mut self, action: &mut Self::ActionKind) { match action { DrawAction::CreatePath { points } => { - let path = self.paths.insert_container(0, ContainerType::Map).unwrap(); - let path_map = path.into_map().unwrap(); - let pos_map = path_map - .insert_container("pos", ContainerType::Map) - .unwrap() - .into_map() - .unwrap(); + let path_map = self.paths.insert_container(0, LoroMap::new()).unwrap(); + let pos_map = path_map.insert_container("pos", LoroMap::new()).unwrap(); pos_map.insert("x", 0).unwrap(); pos_map.insert("y", 0).unwrap(); - let path = path_map - .insert_container("path", ContainerType::List) - .unwrap() - .into_list() - .unwrap(); + let path = path_map.insert_container("path", LoroList::new()).unwrap(); for p in points { - let map = path - .push_container(ContainerType::Map) - .unwrap() - .into_map() - .unwrap(); + let map = path.push_container(LoroMap::new()).unwrap(); map.insert("x", p.x).unwrap(); map.insert("y", p.y).unwrap(); } @@ -68,29 +55,18 @@ impl ActorTrait for DrawActor { self.id_to_obj.insert(len, path_map.id()); } DrawAction::Text { text, pos, size } => { - let text_container = self - .texts - .insert_container(0, ContainerType::Map) - .unwrap() - .into_map() - .unwrap(); + let text_container = self.texts.insert_container(0, LoroMap::new()).unwrap(); let text_inner = text_container - .insert_container("text", ContainerType::Text) - .unwrap() - .into_text() + .insert_container("text", LoroText::new()) .unwrap(); text_inner.insert(0, text).unwrap(); let map = text_container - .insert_container("pos", ContainerType::Map) - .unwrap() - .into_map() + .insert_container("pos", LoroMap::new()) .unwrap(); map.insert("x", pos.x).unwrap(); map.insert("y", pos.y).unwrap(); let map = text_container - .insert_container("size", ContainerType::Map) - .unwrap() - .into_map() + .insert_container("size", LoroMap::new()) .unwrap(); map.insert("x", size.x).unwrap(); map.insert("y", size.y).unwrap(); @@ -99,21 +75,12 @@ impl ActorTrait for DrawActor { self.id_to_obj.insert(len, text_container.id()); } DrawAction::CreateRect { pos, .. } => { - let rect = self.rects.insert_container(0, ContainerType::Map).unwrap(); - let rect_map = rect.into_map().unwrap(); - let pos_map = rect_map - .insert_container("pos", ContainerType::Map) - .unwrap() - .into_map() - .unwrap(); + let rect_map = self.rects.insert_container(0, LoroMap::new()).unwrap(); + let pos_map = rect_map.insert_container("pos", LoroMap::new()).unwrap(); pos_map.insert("x", pos.x).unwrap(); pos_map.insert("y", pos.y).unwrap(); - let size_map = rect_map - .insert_container("size", ContainerType::Map) - .unwrap() - .into_map() - .unwrap(); + let size_map = rect_map.insert_container("size", LoroMap::new()).unwrap(); size_map.insert("width", pos.x).unwrap(); size_map.insert("height", pos.y).unwrap(); diff --git a/crates/examples/src/sheet.rs b/crates/examples/src/sheet.rs index f1f8ce16..f7ad0441 100644 --- a/crates/examples/src/sheet.rs +++ b/crates/examples/src/sheet.rs @@ -1,4 +1,4 @@ -use loro::LoroDoc; +use loro::{LoroDoc, LoroMap}; pub fn init_sheet() -> LoroDoc { let doc = LoroDoc::new(); @@ -6,7 +6,7 @@ pub fn init_sheet() -> LoroDoc { let cols = doc.get_list("cols"); let rows = doc.get_list("rows"); for _ in 0..bench_utils::sheet::SheetAction::MAX_ROW { - rows.push_container(loro::ContainerType::Map).unwrap(); + rows.push_container(LoroMap::new()).unwrap(); } for i in 0..bench_utils::sheet::SheetAction::MAX_COL { diff --git a/crates/fuzz/src/container/list.rs b/crates/fuzz/src/container/list.rs index 452b7d4f..554319c4 100644 --- a/crates/fuzz/src/container/list.rs +++ b/crates/fuzz/src/container/list.rs @@ -105,7 +105,7 @@ impl Actionable for ListAction { let pos = *pos as usize; match value { FuzzValue::Container(c) => { - let container = list.insert_container(pos, *c).unwrap(); + let container = list.insert_container(pos, Container::new(*c)).unwrap(); Some(container) } FuzzValue::I32(v) => { diff --git a/crates/fuzz/src/container/map.rs b/crates/fuzz/src/container/map.rs index 5d3c8b9c..fc46fbcd 100644 --- a/crates/fuzz/src/container/map.rs +++ b/crates/fuzz/src/container/map.rs @@ -119,7 +119,7 @@ impl Actionable for MapAction { None } FuzzValue::Container(c) => { - let container = handler.insert_container(key, *c).unwrap(); + let container = handler.insert_container(key, Container::new(*c)).unwrap(); Some(container) } } diff --git a/crates/fuzz/src/container/tree.rs b/crates/fuzz/src/container/tree.rs index 187b92b4..7d119980 100644 --- a/crates/fuzz/src/container/tree.rs +++ b/crates/fuzz/src/container/tree.rs @@ -169,7 +169,7 @@ impl Actionable for TreeAction { None } FuzzValue::Container(c) => { - let container = meta.insert_container(k, *c).unwrap(); + let container = meta.insert_container(k, Container::new(*c)).unwrap(); Some(container) } } diff --git a/crates/loro-common/src/error.rs b/crates/loro-common/src/error.rs index 514d12e2..e364fab5 100644 --- a/crates/loro-common/src/error.rs +++ b/crates/loro-common/src/error.rs @@ -45,6 +45,12 @@ pub enum LoroError { InvalidFrontierIdNotFound(ID), #[error("Cannot import when the doc is in a transaction")] ImportWhenInTxn, + #[error("The given method ({method}) is not allowed when the container is detached. You should insert the container to the doc first.")] + MisuseDettachedContainer { method: &'static str }, + #[error("Not implemented: {0}")] + NotImplemented(&'static str), + #[error("Reattach a container that is already attached")] + ReattachAttachedContainer, } #[derive(Error, Debug)] diff --git a/crates/loro-internal/benches/event.rs b/crates/loro-internal/benches/event.rs index 9bb508d7..fc5eb487 100644 --- a/crates/loro-internal/benches/event.rs +++ b/crates/loro-internal/benches/event.rs @@ -2,7 +2,7 @@ use criterion::{criterion_group, criterion_main, Criterion}; #[cfg(feature = "test_utils")] mod event { use super::*; - use loro_common::ContainerType; + use loro_internal::{ListHandler, LoroDoc}; use std::sync::Arc; @@ -10,9 +10,7 @@ mod event { let mut ans = vec![]; for idx in 0..children_num { let child_handler = handler - .insert_container(idx, ContainerType::List) - .unwrap() - .into_list() + .insert_container(idx, ListHandler::new_detached()) .unwrap(); ans.push(child_handler); } diff --git a/crates/loro-internal/examples/event.rs b/crates/loro-internal/examples/event.rs index 7f9fb76f..f295628f 100644 --- a/crates/loro-internal/examples/event.rs +++ b/crates/loro-internal/examples/event.rs @@ -1,11 +1,11 @@ use std::sync::Arc; -use loro_common::ContainerType; + use loro_internal::{ delta::DeltaItem, event::Diff, handler::{Handler, ValueOrHandler}, - LoroDoc, ToJson, + ListHandler, LoroDoc, MapHandler, TextHandler, ToJson, TreeHandler, }; fn main() { @@ -30,12 +30,12 @@ fn main() { let text = h .as_map() .unwrap() - .insert_container("text", ContainerType::Text) - .unwrap(); - text.as_text() - .unwrap() - .insert(0, "created from event") + .insert_container( + "text", + TextHandler::new_detached(), + ) .unwrap(); + text.insert(0, "created from event").unwrap(); } } ValueOrHandler::Value(value) => { @@ -54,10 +54,14 @@ fn main() { } })); list.insert(0, "abc").unwrap(); - list.insert_container(1, ContainerType::List).unwrap(); - list.insert_container(2, ContainerType::Map).unwrap(); - list.insert_container(3, ContainerType::Text).unwrap(); - list.insert_container(4, ContainerType::Tree).unwrap(); + list.insert_container(1, ListHandler::new_detached()) + .unwrap(); + list.insert_container(2, MapHandler::new_detached()) + .unwrap(); + list.insert_container(3, TextHandler::new_detached()) + .unwrap(); + list.insert_container(4, TreeHandler::new_detached()) + .unwrap(); doc.commit_then_renew(); assert_eq!( doc.get_deep_value().to_json(), diff --git a/crates/loro-internal/src/container/richtext/richtext_state.rs b/crates/loro-internal/src/container/richtext/richtext_state.rs index 4402378c..0dfce3cf 100644 --- a/crates/loro-internal/src/container/richtext/richtext_state.rs +++ b/crates/loro-internal/src/container/richtext/richtext_state.rs @@ -1099,9 +1099,7 @@ mod cursor_cache { let offset = pos - c.pos; let leaf = tree.get_leaf(c.leaf.into()); - let Some(s) = leaf.elem().as_str() else { - return None; - }; + let s = leaf.elem().as_str()?; let Some(offset) = pos_to_unicode_index(s, offset, pos_type) else { continue; diff --git a/crates/loro-internal/src/fuzz.rs b/crates/loro-internal/src/fuzz.rs index 66da6e28..242a8b9a 100644 --- a/crates/loro-internal/src/fuzz.rs +++ b/crates/loro-internal/src/fuzz.rs @@ -548,7 +548,8 @@ pub fn test_multi_sites(site_num: u8, actions: &mut [Action]) { let _e = s.enter(); let diff = site .get_text("text") - .with_state(|s| s.to_diff(site.arena(), &site.get_global_txn(), &site.weak_state())); + .with_state(|s| Ok(s.to_diff(site.arena(), &site.get_global_txn(), &site.weak_state()))) + .unwrap(); let mut diff = diff.into_text().unwrap(); compact(&mut diff); let mut text = text.lock().unwrap(); diff --git a/crates/loro-internal/src/fuzz/recursive_refactored.rs b/crates/loro-internal/src/fuzz/recursive_refactored.rs index 9f0f7ec2..f97ecb40 100644 --- a/crates/loro-internal/src/fuzz/recursive_refactored.rs +++ b/crates/loro-internal/src/fuzz/recursive_refactored.rs @@ -341,9 +341,9 @@ trait Actionable { } impl Actor { - fn add_new_container(&mut self, idx: ContainerIdx, id: ContainerID, type_: ContainerType) { + fn add_new_container(&mut self, _idx: ContainerIdx, id: ContainerID, type_: ContainerType) { let txn = self.loro.get_global_txn(); - let handler = Handler::new( + let handler = Handler::new_attached( id, self.loro.arena().clone(), txn, @@ -588,7 +588,11 @@ impl Actionable for Vec { } FuzzValue::Container(c) => { let handler = &container - .insert_container_with_txn(&mut txn, &key.to_string(), *c) + .insert_container_with_txn( + &mut txn, + &key.to_string(), + Handler::new_unattached(*c), + ) .unwrap(); let idx = handler.container_idx(); actor.add_new_container(idx, handler.id().clone(), *c); @@ -630,7 +634,11 @@ impl Actionable for Vec { } FuzzValue::Container(c) => { let handler = &container - .insert_container_with_txn(&mut txn, *key as usize, *c) + .insert_container_with_txn( + &mut txn, + *key as usize, + Handler::new_unattached(*c), + ) .unwrap(); let idx = handler.container_idx(); actor.add_new_container(idx, handler.id().clone(), *c); diff --git a/crates/loro-internal/src/handler.rs b/crates/loro-internal/src/handler.rs index e740c87e..15eb2aeb 100644 --- a/crates/loro-internal/src/handler.rs +++ b/crates/loro-internal/src/handler.rs @@ -4,28 +4,335 @@ use crate::{ container::{ idx::ContainerIdx, list::list_op::{DeleteSpan, DeleteSpanWithId, ListOp}, - richtext::richtext_state::PosType, + richtext::{richtext_state::PosType, RichtextState, StyleOp, TextStyleInfoFlag}, tree::tree_op::TreeOp, }, delta::{DeltaItem, StyleMeta, TreeDiffItem, TreeExternalDiff}, op::ListSlice, - state::{State, TreeParentId}, + state::{ContainerState, State, TreeParentId}, txn::EventHint, utils::{string_slice::StringSlice, utf16::count_utf16_len}, }; +use append_only_bytes::BytesSlice; use enum_as_inner::EnumAsInner; use fxhash::FxHashMap; use loro_common::{ - ContainerID, ContainerType, InternalString, LoroError, LoroResult, LoroTreeError, LoroValue, - TreeID, + ContainerID, ContainerType, Counter, IdFull, InternalString, LoroError, LoroResult, + LoroTreeError, LoroValue, PeerID, TreeID, ID, }; use serde::{Deserialize, Serialize}; use std::{ borrow::Cow, + fmt::Debug, ops::Deref, - sync::{Mutex, Weak}, + sync::{Arc, Mutex, Weak}, }; +const INSERT_CONTAINER_VALUE_ARG_ERROR: &str = + "Cannot insert a LoroValue::Container directly. To create child container, use insert_container"; + +pub trait HandlerTrait: Clone + Sized { + fn is_attached(&self) -> bool; + fn attached_handler(&self) -> Option<&BasicHandler>; + fn get_value(&self) -> LoroValue; + fn get_deep_value(&self) -> LoroValue; + fn kind(&self) -> ContainerType; + fn to_handler(&self) -> Handler; + fn from_handler(h: Handler) -> Option; + /// This method returns an attached handler. + fn attach( + &self, + txn: &mut Transaction, + parent: &BasicHandler, + self_id: ContainerID, + ) -> LoroResult; + /// If a detached container is attached, this method will return its corresponding attached handler. + fn get_attached(&self) -> Option; + + fn parent(&self) -> Option { + self.attached_handler().and_then(|x| x.parent()) + } + + fn idx(&self) -> ContainerIdx { + self.attached_handler() + .map(|x| x.container_idx) + .unwrap_or_else(|| ContainerIdx::from_index_and_type(u32::MAX, self.kind())) + } + + fn id(&self) -> ContainerID { + self.attached_handler() + .map(|x| x.id.clone()) + .unwrap_or_else(|| ContainerID::new_normal(ID::NONE_ID, self.kind())) + } + + fn with_state(&self, f: impl FnOnce(&mut State) -> LoroResult) -> LoroResult { + let inner = self + .attached_handler() + .ok_or(LoroError::MisuseDettachedContainer { + method: "with_state", + })?; + let state = inner.state.upgrade().unwrap(); + let mut guard = state.lock().unwrap(); + guard.with_state_mut(inner.container_idx, f) + } +} + +fn create_handler(inner: &BasicHandler, id: ContainerID) -> Handler { + Handler::new_attached( + id, + inner.arena.clone(), + inner.txn.clone(), + inner.state.clone(), + ) +} + +/// Flatten attributes that allow overlap +#[derive(Clone)] +pub struct BasicHandler { + id: ContainerID, + arena: SharedArena, + container_idx: ContainerIdx, + txn: Weak>>, + state: Weak>, +} + +struct DetachedInner { + value: T, + /// If the handler attached later, this field will be filled. + attached: Option, +} + +impl DetachedInner { + fn new(v: T) -> Self { + Self { + value: v, + attached: None, + } + } +} + +enum MaybeDetached { + Detached(Arc>>), + Attached(BasicHandler), +} + +impl Clone for MaybeDetached { + fn clone(&self) -> Self { + match self { + MaybeDetached::Detached(a) => MaybeDetached::Detached(Arc::clone(a)), + MaybeDetached::Attached(a) => MaybeDetached::Attached(a.clone()), + } + } +} + +impl MaybeDetached { + fn new_detached(v: T) -> Self { + MaybeDetached::Detached(Arc::new(Mutex::new(DetachedInner::new(v)))) + } + + fn is_attached(&self) -> bool { + match self { + MaybeDetached::Detached(_) => false, + MaybeDetached::Attached(_) => true, + } + } + + fn attached_handler(&self) -> Option<&BasicHandler> { + match self { + MaybeDetached::Detached(_) => None, + MaybeDetached::Attached(a) => Some(a), + } + } + + fn try_attached_state(&self) -> LoroResult<&BasicHandler> { + match self { + MaybeDetached::Detached(_) => Err(LoroError::MisuseDettachedContainer { + method: "inner_state", + }), + MaybeDetached::Attached(a) => Ok(a), + } + } +} + +impl From for MaybeDetached { + fn from(a: BasicHandler) -> Self { + MaybeDetached::Attached(a) + } +} + +impl BasicHandler { + #[inline] + fn with_doc_state(&self, f: impl FnOnce(&mut DocState) -> R) -> R { + let state = self.state.upgrade().unwrap(); + let mut guard = state.lock().unwrap(); + f(&mut guard) + } + + fn with_txn( + &self, + f: impl FnOnce(&mut Transaction) -> Result, + ) -> Result { + with_txn(&self.txn, f) + } + + fn get_parent(&self) -> Option { + let parent_idx = self.arena.get_parent(self.container_idx)?; + let parent_id = self.arena.get_container_id(parent_idx).unwrap(); + { + let arena = self.arena.clone(); + let txn = self.txn.clone(); + let state = self.state.clone(); + let kind = parent_id.container_type(); + let handler = BasicHandler { + container_idx: parent_idx, + id: parent_id, + txn, + arena, + state, + }; + + Some(match kind { + ContainerType::Map => Handler::Map(MapHandler { + inner: handler.into(), + }), + ContainerType::List => Handler::List(ListHandler { + inner: handler.into(), + }), + ContainerType::Tree => Handler::Tree(TreeHandler { + inner: handler.into(), + }), + ContainerType::Text => Handler::Text(TextHandler { + inner: handler.into(), + }), + }) + } + } + + pub fn get_value(&self) -> LoroValue { + self.state + .upgrade() + .unwrap() + .lock() + .unwrap() + .get_value_by_idx(self.container_idx) + } + + pub fn get_deep_value(&self) -> LoroValue { + self.state + .upgrade() + .unwrap() + .lock() + .unwrap() + .get_container_deep_value(self.container_idx) + } + + fn with_state(&self, f: impl FnOnce(&mut State) -> R) -> R { + let state = self.state.upgrade().unwrap(); + let mut guard = state.lock().unwrap(); + guard.with_state_mut(self.container_idx, f) + } + + pub fn parent(&self) -> Option { + self.get_parent() + } +} + +/// Flatten attributes that allow overlap +#[derive(Clone)] +pub struct TextHandler { + inner: MaybeDetached, +} + +impl HandlerTrait for TextHandler { + fn to_handler(&self) -> Handler { + Handler::Text(self.clone()) + } + + fn attach( + &self, + txn: &mut Transaction, + parent: &BasicHandler, + self_id: ContainerID, + ) -> LoroResult { + match &self.inner { + MaybeDetached::Detached(t) => { + let mut t = t.try_lock().unwrap(); + let inner = create_handler(parent, self_id); + let text = inner.into_text().unwrap(); + let mut delta: Vec = Vec::new(); + for span in t.value.iter() { + delta.push(TextDelta::Insert { + insert: span.text.to_string(), + attributes: span.attributes.to_option_map(), + }); + } + + text.apply_delta_with_txn(txn, &delta)?; + t.attached = text.attached_handler().cloned(); + Ok(text) + } + MaybeDetached::Attached(_a) => unreachable!(), + } + } + + fn attached_handler(&self) -> Option<&BasicHandler> { + self.inner.attached_handler() + } + + fn get_value(&self) -> LoroValue { + match &self.inner { + MaybeDetached::Detached(t) => { + let t = t.try_lock().unwrap(); + LoroValue::String(Arc::new(t.value.to_string())) + } + MaybeDetached::Attached(a) => { + a.with_state(|state| state.as_richtext_state_mut().unwrap().get_value()) + } + } + } + + fn get_deep_value(&self) -> LoroValue { + self.get_value() + } + + fn is_attached(&self) -> bool { + matches!(&self.inner, MaybeDetached::Attached(..)) + } + + fn kind(&self) -> ContainerType { + ContainerType::Text + } + + fn get_attached(&self) -> Option { + match &self.inner { + MaybeDetached::Detached(d) => d.lock().unwrap().attached.clone().map(|x| Self { + inner: MaybeDetached::Attached(x), + }), + MaybeDetached::Attached(_a) => Some(self.clone()), + } + } + + fn from_handler(h: Handler) -> Option { + match h { + Handler::Text(x) => Some(x), + _ => None, + } + } +} + +impl std::fmt::Debug for TextHandler { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.inner { + MaybeDetached::Detached(_) => { + write!(f, "TextHandler(Unattached)") + } + MaybeDetached::Attached(a) => { + write!(f, "TextHandler({:?})", &a.id) + } + } + } +} + #[derive(Debug, Clone, EnumAsInner, Deserialize, Serialize, PartialEq)] #[serde(untagged)] pub enum TextDelta { @@ -61,179 +368,347 @@ impl From<&DeltaItem> for TextDelta { } } -pub trait HandlerTrait { - fn inner(&self) -> &BasicHandler; - - fn get_value(&self) -> LoroValue { - self.inner() - .state - .upgrade() - .unwrap() - .lock() - .unwrap() - .get_value_by_idx(self.inner().container_idx) - } - - fn get_deep_value(&self) -> LoroValue { - self.inner() - .state - .upgrade() - .unwrap() - .lock() - .unwrap() - .get_container_deep_value(self.inner().container_idx) - } - - fn idx(&self) -> ContainerIdx { - self.inner().container_idx - } - - fn id(&self) -> &ContainerID { - &self.inner().id - } - - fn with_state(&self, f: impl FnOnce(&mut State) -> R) -> R { - let state = self.inner().state.upgrade().unwrap(); - let mut guard = state.lock().unwrap(); - guard.with_state_mut(self.idx(), f) - } - - fn with_txn(&self, f: impl FnOnce(&mut Transaction) -> LoroResult) -> LoroResult { - with_txn(&self.inner().txn, f) - } - - fn parent(&self) -> Option { - self.inner().get_parent() - } -} - -fn create_handler(handler: &impl HandlerTrait, id: ContainerID) -> Handler { - Handler::new( - id, - handler.inner().arena.clone(), - handler.inner().txn.clone(), - handler.inner().state.clone(), - ) -} - -/// Flatten attributes that allow overlap -#[derive(Clone)] -pub struct BasicHandler { - id: ContainerID, - arena: SharedArena, - container_idx: ContainerIdx, - txn: Weak>>, - state: Weak>, -} - -impl BasicHandler { - #[inline] - fn with_doc_state(&self, f: impl FnOnce(&mut DocState) -> R) -> R { - let state = self.state.upgrade().unwrap(); - let mut guard = state.lock().unwrap(); - f(&mut guard) - } - - fn with_txn( - &self, - f: impl FnOnce(&mut Transaction) -> Result<(), LoroError>, - ) -> Result<(), LoroError> { - with_txn(&self.txn, f) - } - - fn get_parent(&self) -> Option { - let parent_idx = self.arena.get_parent(self.container_idx)?; - let parent_id = self.arena.get_container_id(parent_idx).unwrap(); - { - let arena = self.arena.clone(); - let txn = self.txn.clone(); - let state = self.state.clone(); - let kind = parent_id.container_type(); - let handler = BasicHandler { - container_idx: parent_idx, - id: parent_id, - txn, - arena, - state, - }; - - Some(match kind { - ContainerType::Map => Handler::Map(MapHandler { inner: handler }), - ContainerType::List => Handler::List(ListHandler { inner: handler }), - ContainerType::Tree => Handler::Tree(TreeHandler { inner: handler }), - ContainerType::Text => Handler::Text(TextHandler { inner: handler }), - }) - } - } -} - -/// Flatten attributes that allow overlap -#[derive(Clone)] -pub struct TextHandler { - inner: BasicHandler, -} - -impl HandlerTrait for TextHandler { - fn inner(&self) -> &BasicHandler { - &self.inner - } -} - -impl std::fmt::Debug for TextHandler { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "TextHandler {}", self.inner.id) - } -} - #[derive(Clone)] pub struct MapHandler { - inner: BasicHandler, + inner: MaybeDetached>, } impl HandlerTrait for MapHandler { - fn inner(&self) -> &BasicHandler { - &self.inner + fn is_attached(&self) -> bool { + matches!(&self.inner, MaybeDetached::Attached(..)) + } + + fn attached_handler(&self) -> Option<&BasicHandler> { + match &self.inner { + MaybeDetached::Detached(_) => None, + MaybeDetached::Attached(a) => Some(a), + } + } + + fn get_value(&self) -> LoroValue { + match &self.inner { + MaybeDetached::Detached(m) => { + let m = m.try_lock().unwrap(); + let mut map = FxHashMap::default(); + for (k, v) in m.value.iter() { + map.insert(k.to_string(), v.to_value()); + } + LoroValue::Map(Arc::new(map)) + } + MaybeDetached::Attached(a) => a.get_value(), + } + } + + fn get_deep_value(&self) -> LoroValue { + match &self.inner { + MaybeDetached::Detached(m) => { + let m = m.try_lock().unwrap(); + let mut map = FxHashMap::default(); + for (k, v) in m.value.iter() { + map.insert(k.to_string(), v.to_deep_value()); + } + LoroValue::Map(Arc::new(map)) + } + MaybeDetached::Attached(a) => a.get_deep_value(), + } + } + + fn kind(&self) -> ContainerType { + ContainerType::Map + } + + fn to_handler(&self) -> Handler { + Handler::Map(self.clone()) + } + + fn attach( + &self, + txn: &mut Transaction, + parent: &BasicHandler, + self_id: ContainerID, + ) -> LoroResult { + match &self.inner { + MaybeDetached::Detached(m) => { + let mut m = m.try_lock().unwrap(); + let inner = create_handler(parent, self_id); + let map = inner.into_map().unwrap(); + for (k, v) in m.value.iter() { + match v { + ValueOrHandler::Value(v) => { + map.insert_with_txn(txn, k, v.clone())?; + } + ValueOrHandler::Handler(h) => { + map.insert_container_with_txn(txn, k, h.clone())?; + } + } + } + m.attached = map.attached_handler().cloned(); + Ok(map) + } + MaybeDetached::Attached(_a) => unreachable!(), + } + } + + fn get_attached(&self) -> Option { + match &self.inner { + MaybeDetached::Detached(d) => d.lock().unwrap().attached.clone().map(|x| Self { + inner: MaybeDetached::Attached(x), + }), + MaybeDetached::Attached(_a) => Some(self.clone()), + } + } + + fn from_handler(h: Handler) -> Option { + match h { + Handler::Map(x) => Some(x), + _ => None, + } } } impl std::fmt::Debug for MapHandler { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "MapHandler {}", self.inner.id) + match &self.inner { + MaybeDetached::Detached(_) => write!(f, "MapHandler Dettached"), + MaybeDetached::Attached(a) => write!(f, "MapHandler {}", a.id), + } } } #[derive(Clone)] pub struct ListHandler { - inner: BasicHandler, + inner: MaybeDetached>, } impl std::fmt::Debug for ListHandler { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "ListHandler {}", self.inner.id) + match &self.inner { + MaybeDetached::Detached(_) => write!(f, "ListHandler Dettached"), + MaybeDetached::Attached(a) => write!(f, "ListHandler {}", a.id), + } } } impl HandlerTrait for ListHandler { - fn inner(&self) -> &BasicHandler { - &self.inner + fn is_attached(&self) -> bool { + self.inner.is_attached() + } + + fn attached_handler(&self) -> Option<&BasicHandler> { + self.inner.attached_handler() + } + + fn get_value(&self) -> LoroValue { + match &self.inner { + MaybeDetached::Detached(a) => { + let a = a.try_lock().unwrap(); + LoroValue::List(Arc::new(a.value.iter().map(|v| v.to_value()).collect())) + } + MaybeDetached::Attached(a) => a.get_value(), + } + } + + fn get_deep_value(&self) -> LoroValue { + match &self.inner { + MaybeDetached::Detached(a) => { + let a = a.try_lock().unwrap(); + LoroValue::List(Arc::new( + a.value.iter().map(|v| v.to_deep_value()).collect(), + )) + } + MaybeDetached::Attached(a) => a.get_deep_value(), + } + } + + fn kind(&self) -> ContainerType { + ContainerType::List + } + + fn to_handler(&self) -> Handler { + Handler::List(self.clone()) + } + + fn attach( + &self, + txn: &mut Transaction, + parent: &BasicHandler, + self_id: ContainerID, + ) -> LoroResult { + match &self.inner { + MaybeDetached::Detached(l) => { + let mut l = l.try_lock().unwrap(); + let inner = create_handler(parent, self_id); + let list = inner.into_list().unwrap(); + for (index, v) in l.value.iter().enumerate() { + match v { + ValueOrHandler::Value(v) => { + list.insert_with_txn(txn, index, v.clone())?; + } + ValueOrHandler::Handler(h) => { + list.insert_container_with_txn(txn, index, h.clone())?; + } + } + } + l.attached = list.attached_handler().cloned(); + Ok(list) + } + MaybeDetached::Attached(_a) => unreachable!(), + } + } + + fn get_attached(&self) -> Option { + match &self.inner { + MaybeDetached::Detached(d) => d.lock().unwrap().attached.clone().map(|x| Self { + inner: MaybeDetached::Attached(x), + }), + MaybeDetached::Attached(_a) => Some(self.clone()), + } + } + + fn from_handler(h: Handler) -> Option { + match h { + Handler::List(x) => Some(x), + _ => None, + } } } /// #[derive(Clone)] pub struct TreeHandler { - inner: BasicHandler, + inner: MaybeDetached, +} + +#[derive(Clone)] +struct TreeInner { + next_counter: Counter, + map: FxHashMap, + parent_links: FxHashMap>, +} + +impl TreeInner { + fn new() -> Self { + TreeInner { + next_counter: 0, + map: FxHashMap::default(), + parent_links: FxHashMap::default(), + } + } + + fn create(&mut self, parent: Option) -> TreeID { + let id = TreeID::new(PeerID::MAX, self.next_counter); + self.next_counter += 1; + self.map.insert( + id, + Handler::new_unattached(ContainerType::Map) + .into_map() + .unwrap(), + ); + self.parent_links.insert(id, parent); + id + } + + fn delete(&mut self, id: TreeID) { + self.map.remove(&id); + self.parent_links.remove(&id); + } + + fn get_parent(&self, id: TreeID) -> Option> { + self.parent_links.get(&id).cloned() + } + + fn mov(&mut self, target: TreeID, new_parent: Option) { + let old = self.parent_links.insert(target, new_parent); + assert!(old.is_some()); + } + + fn get_children(&self, id: TreeID) -> Option> { + let mut children = Vec::new(); + for (c, p) in &self.parent_links { + if p.as_ref() == Some(&id) { + children.push(*c); + } + } + Some(children) + } } impl HandlerTrait for TreeHandler { - fn inner(&self) -> &BasicHandler { - &self.inner + fn to_handler(&self) -> Handler { + Handler::Tree(self.clone()) + } + + fn attach( + &self, + _txn: &mut Transaction, + parent: &BasicHandler, + self_id: ContainerID, + ) -> LoroResult { + match &self.inner { + MaybeDetached::Detached(t) => { + let mut t = t.try_lock().unwrap(); + let inner = create_handler(parent, self_id); + let tree = inner.into_tree().unwrap(); + if t.value.map.is_empty() { + t.attached = tree.attached_handler().cloned(); + Ok(tree) + } else { + unimplemented!("attach detached tree"); + } + } + MaybeDetached::Attached(_a) => unreachable!(), + } + } + + fn is_attached(&self) -> bool { + self.inner.is_attached() + } + + fn attached_handler(&self) -> Option<&BasicHandler> { + self.inner.attached_handler() + } + + fn get_value(&self) -> LoroValue { + match &self.inner { + MaybeDetached::Detached(_) => unimplemented!(), + MaybeDetached::Attached(a) => a.get_value(), + } + } + + fn get_deep_value(&self) -> LoroValue { + match &self.inner { + MaybeDetached::Detached(_) => unimplemented!(), + MaybeDetached::Attached(a) => a.get_deep_value(), + } + } + + fn kind(&self) -> ContainerType { + ContainerType::Tree + } + + fn get_attached(&self) -> Option { + match &self.inner { + MaybeDetached::Detached(d) => d.lock().unwrap().attached.clone().map(|x| Self { + inner: MaybeDetached::Attached(x), + }), + MaybeDetached::Attached(_a) => Some(self.clone()), + } + } + + fn from_handler(h: Handler) -> Option { + match h { + Handler::Tree(x) => Some(x), + _ => None, + } } } impl std::fmt::Debug for TreeHandler { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "TreeHandler {}", self.inner.id) + match &self.inner { + MaybeDetached::Detached(_) => write!(f, "TreeHandler Dettached"), + MaybeDetached::Attached(a) => write!(f, "TreeHandler {}", a.id), + } } } @@ -245,8 +720,91 @@ pub enum Handler { Tree(TreeHandler), } +impl HandlerTrait for Handler { + fn is_attached(&self) -> bool { + match self { + Self::Text(x) => x.is_attached(), + Self::Map(x) => x.is_attached(), + Self::List(x) => x.is_attached(), + Self::Tree(x) => x.is_attached(), + } + } + + fn attached_handler(&self) -> Option<&BasicHandler> { + match self { + Self::Text(x) => x.attached_handler(), + Self::Map(x) => x.attached_handler(), + Self::List(x) => x.attached_handler(), + Self::Tree(x) => x.attached_handler(), + } + } + + fn get_value(&self) -> LoroValue { + match self { + Self::Text(x) => x.get_value(), + Self::Map(x) => x.get_value(), + Self::List(x) => x.get_value(), + Self::Tree(x) => x.get_value(), + } + } + + fn get_deep_value(&self) -> LoroValue { + match self { + Self::Text(x) => x.get_deep_value(), + Self::Map(x) => x.get_deep_value(), + Self::List(x) => x.get_deep_value(), + Self::Tree(x) => x.get_deep_value(), + } + } + + fn kind(&self) -> ContainerType { + match self { + Self::Text(x) => x.kind(), + Self::Map(x) => x.kind(), + Self::List(x) => x.kind(), + Self::Tree(x) => x.kind(), + } + } + + fn to_handler(&self) -> Handler { + match self { + Self::Text(x) => x.to_handler(), + Self::Map(x) => x.to_handler(), + Self::List(x) => x.to_handler(), + Self::Tree(x) => x.to_handler(), + } + } + + fn attach( + &self, + txn: &mut Transaction, + parent: &BasicHandler, + self_id: ContainerID, + ) -> LoroResult { + match self { + Self::Text(x) => Ok(Handler::Text(x.attach(txn, parent, self_id)?)), + Self::Map(x) => Ok(Handler::Map(x.attach(txn, parent, self_id)?)), + Self::List(x) => Ok(Handler::List(x.attach(txn, parent, self_id)?)), + Self::Tree(x) => Ok(Handler::Tree(x.attach(txn, parent, self_id)?)), + } + } + + fn get_attached(&self) -> Option { + match self { + Self::Text(x) => x.get_attached().map(Handler::Text), + Self::Map(x) => x.get_attached().map(Handler::Map), + Self::List(x) => x.get_attached().map(Handler::List), + Self::Tree(x) => x.get_attached().map(Handler::Tree), + } + } + + fn from_handler(h: Handler) -> Option { + Some(h) + } +} + impl Handler { - pub(crate) fn new( + pub(crate) fn new_attached( id: ContainerID, arena: SharedArena, txn: Weak>>, @@ -262,28 +820,45 @@ impl Handler { }; match kind { - ContainerType::Map => Self::Map(MapHandler { inner: handler }), - ContainerType::List => Self::List(ListHandler { inner: handler }), - ContainerType::Tree => Self::Tree(TreeHandler { inner: handler }), - ContainerType::Text => Self::Text(TextHandler { inner: handler }), + ContainerType::Map => Self::Map(MapHandler { + inner: handler.into(), + }), + ContainerType::List => Self::List(ListHandler { + inner: handler.into(), + }), + ContainerType::Tree => Self::Tree(TreeHandler { + inner: handler.into(), + }), + ContainerType::Text => Self::Text(TextHandler { + inner: handler.into(), + }), } } - pub fn id(&self) -> &ContainerID { - match self { - Self::Map(x) => &x.inner.id, - Self::List(x) => &x.inner.id, - Self::Text(x) => &x.inner.id, - Self::Tree(x) => &x.inner.id, + pub(crate) fn new_unattached(kind: ContainerType) -> Self { + match kind { + ContainerType::Text => Self::Text(TextHandler::new_detached()), + ContainerType::Map => Self::Map(MapHandler::new_detached()), + ContainerType::List => Self::List(ListHandler::new_detached()), + ContainerType::Tree => Self::Tree(TreeHandler::new_detached()), } } - pub fn container_idx(&self) -> ContainerIdx { + pub fn id(&self) -> ContainerID { match self { - Self::Map(x) => x.inner.container_idx, - Self::List(x) => x.inner.container_idx, - Self::Text(x) => x.inner.container_idx, - Self::Tree(x) => x.inner.container_idx, + Self::Map(x) => x.id(), + Self::List(x) => x.id(), + Self::Text(x) => x.id(), + Self::Tree(x) => x.id(), + } + } + + pub(crate) fn container_idx(&self) -> ContainerIdx { + match self { + Self::Map(x) => x.idx(), + Self::List(x) => x.idx(), + Self::Text(x) => x.idx(), + Self::Tree(x) => x.idx(), } } @@ -295,6 +870,15 @@ impl Handler { Self::Tree(_) => ContainerType::Tree, } } + + fn get_deep_value(&self) -> LoroValue { + match self { + Self::Map(x) => x.get_deep_value(), + Self::List(x) => x.get_deep_value(), + Self::Text(x) => x.get_deep_value(), + Self::Tree(x) => x.get_deep_value(), + } + } } #[derive(Clone, EnumAsInner, Debug)] @@ -311,50 +895,120 @@ impl ValueOrHandler { state: &Weak>, ) -> Self { if let LoroValue::Container(c) = value { - ValueOrHandler::Handler(Handler::new(c, arena.clone(), txn.clone(), state.clone())) + ValueOrHandler::Handler(Handler::new_attached( + c, + arena.clone(), + txn.clone(), + state.clone(), + )) } else { ValueOrHandler::Value(value) } } + + fn to_value(&self) -> LoroValue { + match self { + Self::Value(v) => v.clone(), + Self::Handler(h) => LoroValue::Container(h.id()), + } + } + + fn to_deep_value(&self) -> LoroValue { + match self { + Self::Value(v) => v.clone(), + Self::Handler(h) => h.get_deep_value(), + } + } } impl TextHandler { + /// Create a new container that is detached from the document. + /// + /// The edits on a detached container will not be persisted. + /// To attach the container to the document, please insert it into an attached container. + pub fn new_detached() -> Self { + Self { + inner: MaybeDetached::new_detached(RichtextState::default()), + } + } + pub fn get_richtext_value(&self) -> LoroValue { - self.with_state(|state| state.as_richtext_state_mut().unwrap().get_richtext_value()) + match &self.inner { + MaybeDetached::Detached(t) => { + let t = t.try_lock().unwrap(); + t.value.get_richtext_value() + } + MaybeDetached::Attached(a) => { + a.with_state(|state| state.as_richtext_state_mut().unwrap().get_richtext_value()) + } + } } pub fn is_empty(&self) -> bool { - self.len_unicode() == 0 + match &self.inner { + MaybeDetached::Detached(t) => t.try_lock().unwrap().value.is_empty(), + MaybeDetached::Attached(a) => { + a.with_state(|state| state.as_richtext_state_mut().unwrap().is_empty()) + } + } } pub fn len_utf8(&self) -> usize { - self.with_state(|state| state.as_richtext_state_mut().unwrap().len_utf8()) + match &self.inner { + MaybeDetached::Detached(t) => { + let t = t.try_lock().unwrap(); + t.value.len_utf8() + } + MaybeDetached::Attached(a) => { + a.with_state(|state| state.as_richtext_state_mut().unwrap().len_utf8()) + } + } } pub fn len_utf16(&self) -> usize { - self.with_state(|state| state.as_richtext_state_mut().unwrap().len_utf16()) + match &self.inner { + MaybeDetached::Detached(t) => { + let t = t.try_lock().unwrap(); + t.value.len_utf16() + } + MaybeDetached::Attached(a) => { + a.with_state(|state| state.as_richtext_state_mut().unwrap().len_utf16()) + } + } } pub fn len_unicode(&self) -> usize { - self.with_state(|state| state.as_richtext_state_mut().unwrap().len_unicode()) + match &self.inner { + MaybeDetached::Detached(t) => { + let t = t.try_lock().unwrap(); + t.value.len_unicode() + } + MaybeDetached::Attached(a) => { + a.with_state(|state| state.as_richtext_state_mut().unwrap().len_unicode()) + } + } } /// if `wasm` feature is enabled, it is a UTF-16 length /// otherwise, it is a Unicode length pub fn len_event(&self) -> usize { - self.with_state(|state| { - if cfg!(feature = "wasm") { - state.as_richtext_state_mut().unwrap().len_utf16() - } else { - state.as_richtext_state_mut().unwrap().len_unicode() - } - }) + if cfg!(feature = "wasm") { + self.len_utf16() + } else { + self.len_unicode() + } } pub fn diagnose(&self) { - todo!(); - - // self.inner.diagnose(); + match &self.inner { + MaybeDetached::Detached(t) => { + let t = t.try_lock().unwrap(); + t.value.diagnose(); + } + MaybeDetached::Attached(a) => { + a.with_state(|state| state.as_richtext_state_mut().unwrap().diagnose()); + } + } } /// `pos` is a Event Index: @@ -364,7 +1018,21 @@ impl TextHandler { /// /// This method requires auto_commit to be enabled. pub fn insert(&self, pos: usize, s: &str) -> LoroResult<()> { - self.inner.with_txn(|txn| self.insert_with_txn(txn, pos, s)) + match &self.inner { + MaybeDetached::Detached(t) => { + let mut t = t.try_lock().unwrap(); + let index = t + .value + .get_entity_index_for_text_insert(pos, PosType::Event); + t.value.insert_at_entity_index( + index, + BytesSlice::from_bytes(s.as_bytes()), + IdFull::NONE_ID, + ); + Ok(()) + } + MaybeDetached::Attached(a) => a.with_txn(|txn| self.insert_with_txn(txn, pos, s)), + } } /// `pos` is a Event Index: @@ -396,7 +1064,8 @@ impl TextHandler { }); } - let (entity_index, styles) = self.with_state(|state| { + let inner = self.inner.try_attached_state()?; + let (entity_index, styles) = inner.with_state(|state| { let richtext_state = state.as_richtext_state_mut().unwrap(); let pos = richtext_state.get_entity_index_for_text_insert(pos); let styles = richtext_state.get_styles_at_entity_index(pos); @@ -434,7 +1103,7 @@ impl TextHandler { }; txn.apply_local_op( - self.inner.container_idx, + inner.container_idx, crate::op::RawOpContent::List(crate::container::list::list_op::ListOp::Insert { slice: ListSlice::RawStr { str: Cow::Borrowed(s), @@ -448,7 +1117,7 @@ impl TextHandler { unicode_len: unicode_len as u32, event_len: event_len as u32, }, - &self.inner.state, + &inner.state, )?; Ok(override_styles) @@ -461,8 +1130,18 @@ impl TextHandler { /// /// This method requires auto_commit to be enabled. pub fn delete(&self, pos: usize, len: usize) -> LoroResult<()> { - self.inner - .with_txn(|txn| self.delete_with_txn(txn, pos, len)) + match &self.inner { + MaybeDetached::Detached(t) => { + let mut t = t.try_lock().unwrap(); + let ranges = t.value.get_text_entity_ranges(pos, len, PosType::Event); + for range in ranges.iter().rev() { + t.value + .drain_by_entity_index(range.entity_start, range.entity_len(), None); + } + Ok(()) + } + MaybeDetached::Attached(a) => a.with_txn(|txn| self.delete_with_txn(txn, pos, len)), + } } /// `pos` is a Event Index: @@ -481,9 +1160,10 @@ impl TextHandler { }); } + let inner = self.inner.try_attached_state()?; let s = tracing::span!(tracing::Level::INFO, "delete pos={} len={}", pos, len); let _e = s.enter(); - let ranges = self.with_state(|state| { + let ranges = inner.with_state(|state| { let richtext_state = state.as_richtext_state_mut().unwrap(); richtext_state.get_text_entity_ranges_in_event_index_range(pos, len) }); @@ -493,7 +1173,7 @@ impl TextHandler { for range in ranges.iter().rev() { let event_start = event_end - range.event_len as isize; txn.apply_local_op( - self.inner.container_idx, + inner.container_idx, crate::op::RawOpContent::List(ListOp::Delete(DeleteSpanWithId::new( range.id_start, range.entity_start as isize, @@ -506,7 +1186,7 @@ impl TextHandler { }, unicode_len: range.entity_len(), }, - &self.inner.state, + &inner.state, )?; event_end = event_start; } @@ -527,8 +1207,59 @@ impl TextHandler { key: impl Into, value: LoroValue, ) -> LoroResult<()> { - self.inner - .with_txn(|txn| self.mark_with_txn(txn, start, end, key, value, false)) + match &self.inner { + MaybeDetached::Detached(t) => { + self.mark_for_detached(&mut t.lock().unwrap().value, key, &value, start, end, false) + } + MaybeDetached::Attached(a) => { + a.with_txn(|txn| self.mark_with_txn(txn, start, end, key, value, false)) + } + } + } + + fn mark_for_detached( + &self, + state: &mut RichtextState, + key: impl Into, + value: &LoroValue, + start: usize, + end: usize, + is_delete: bool, + ) -> Result<(), LoroError> { + let key: InternalString = key.into(); + let len = self.len_event(); + if start >= end { + return Err(loro_common::LoroError::ArgErr( + "Start must be less than end".to_string().into_boxed_str(), + )); + } + if end > len { + return Err(LoroError::OutOfBound { pos: end, len }); + } + let (entity_range, styles) = + state.get_entity_range_and_text_styles_at_range(start..end, PosType::Event); + if let Some(styles) = styles { + if styles.has_key_value(&key, value) { + // already has the same style, skip + return Ok(()); + } + } + + let style_op = Arc::new(StyleOp { + lamport: 0, + peer: 0, + cnt: 0, + key, + value: value.clone(), + // TODO: describe this behavior in the document + info: if is_delete { + TextStyleInfoFlag::BOLD.to_delete() + } else { + TextStyleInfoFlag::BOLD + }, + }); + state.mark_with_entity_index(entity_range, style_op); + Ok(()) } /// `start` and `end` are [Event Index]s: @@ -543,8 +1274,19 @@ impl TextHandler { end: usize, key: impl Into, ) -> LoroResult<()> { - self.inner - .with_txn(|txn| self.mark_with_txn(txn, start, end, key, LoroValue::Null, true)) + match &self.inner { + MaybeDetached::Detached(t) => self.mark_for_detached( + &mut t.lock().unwrap().value, + key, + &LoroValue::Null, + start, + end, + true, + ), + MaybeDetached::Attached(a) => { + a.with_txn(|txn| self.mark_with_txn(txn, start, end, key, LoroValue::Null, true)) + } + } } /// `start` and `end` are [Event Index]s: @@ -571,11 +1313,12 @@ impl TextHandler { return Err(LoroError::OutOfBound { pos: end, len }); } + let inner = self.inner.try_attached_state()?; let key: InternalString = key.into(); - let mutex = &self.inner.state.upgrade().unwrap(); + let mutex = &inner.state.upgrade().unwrap(); let mut doc_state = mutex.lock().unwrap(); - let (entity_range, skip) = doc_state.with_state_mut(self.inner.container_idx, |state| { + let (entity_range, skip) = doc_state.with_state_mut(inner.container_idx, |state| { let (entity_range, styles) = state .as_richtext_state_mut() .unwrap() @@ -611,7 +1354,7 @@ impl TextHandler { drop(style_config); drop(doc_state); txn.apply_local_op( - self.inner.container_idx, + inner.container_idx, crate::op::RawOpContent::List(ListOp::StyleStart { start: entity_start as u32, end: entity_end as u32, @@ -624,31 +1367,45 @@ impl TextHandler { end: end as u32, style: crate::container::richtext::Style { key, data: value }, }, - &self.inner.state, + &inner.state, )?; txn.apply_local_op( - self.inner.container_idx, + inner.container_idx, crate::op::RawOpContent::List(ListOp::StyleEnd), EventHint::MarkEnd, - &self.inner.state, + &inner.state, )?; Ok(()) } pub fn check(&self) { - self.with_state(|state| { - state - .as_richtext_state_mut() - .unwrap() - .check_consistency_between_content_and_style_ranges() - }) + match &self.inner { + MaybeDetached::Detached(t) => { + let t = t.try_lock().unwrap(); + t.value.check_consistency_between_content_and_style_ranges(); + } + MaybeDetached::Attached(a) => a.with_state(|state| { + state + .as_richtext_state_mut() + .unwrap() + .check_consistency_between_content_and_style_ranges(); + }), + } } pub fn apply_delta(&self, delta: &[TextDelta]) -> LoroResult<()> { - self.inner - .with_txn(|txn| self.apply_delta_with_txn(txn, delta)) + match &self.inner { + MaybeDetached::Detached(t) => { + let _t = t.try_lock().unwrap(); + // TODO: implement + Err(LoroError::NotImplemented( + "`apply_delta` on a detached text container", + )) + } + MaybeDetached::Attached(a) => a.with_txn(|txn| self.apply_delta_with_txn(txn, delta)), + } } pub fn apply_delta_with_txn( @@ -708,7 +1465,12 @@ impl TextHandler { #[allow(clippy::inherent_to_string)] pub fn to_string(&self) -> String { - self.with_state(|s| s.as_richtext_state_mut().unwrap().to_string_mut()) + match &self.inner { + MaybeDetached::Detached(t) => t.try_lock().unwrap().value.to_string(), + MaybeDetached::Attached(a) => { + a.with_state(|s| s.as_richtext_state_mut().unwrap().to_string_mut()) + } + } } } @@ -721,9 +1483,26 @@ fn event_len(s: &str) -> usize { } impl ListHandler { + /// Create a new container that is detached from the document. + /// The edits on a detached container will not be persisted. + /// To attach the container to the document, please insert it into an attached container. + pub fn new_detached() -> Self { + Self { + inner: MaybeDetached::new_detached(Vec::new()), + } + } + pub fn insert(&self, pos: usize, v: impl Into) -> LoroResult<()> { - self.inner - .with_txn(|txn| self.insert_with_txn(txn, pos, v.into())) + match &self.inner { + MaybeDetached::Detached(l) => { + let mut list = l.try_lock().unwrap(); + list.value.insert(pos, ValueOrHandler::Value(v.into())); + Ok(()) + } + MaybeDetached::Attached(a) => { + a.with_txn(|txn| self.insert_with_txn(txn, pos, v.into())) + } + } } pub fn insert_with_txn( @@ -739,24 +1518,35 @@ impl ListHandler { }); } - if let Some(container) = v.as_container() { - self.insert_container_with_txn(txn, pos, container.container_type())?; - return Ok(()); + let inner = self.inner.try_attached_state()?; + if let Some(_container) = v.as_container() { + return Err(LoroError::ArgErr( + INSERT_CONTAINER_VALUE_ARG_ERROR + .to_string() + .into_boxed_str(), + )); } txn.apply_local_op( - self.inner.container_idx, + inner.container_idx, crate::op::RawOpContent::List(crate::container::list::list_op::ListOp::Insert { slice: ListSlice::RawData(Cow::Owned(vec![v.clone()])), pos, }), EventHint::InsertList { len: 1 }, - &self.inner.state, + &inner.state, ) } pub fn push(&self, v: LoroValue) -> LoroResult<()> { - with_txn(&self.inner.txn, |txn| self.push_with_txn(txn, v)) + match &self.inner { + MaybeDetached::Detached(l) => { + let mut list = l.try_lock().unwrap(); + list.value.push(ValueOrHandler::Value(v.clone())); + Ok(()) + } + MaybeDetached::Attached(a) => a.with_txn(|txn| self.push_with_txn(txn, v)), + } } pub fn push_with_txn(&self, txn: &mut Transaction, v: LoroValue) -> LoroResult<()> { @@ -765,7 +1555,13 @@ impl ListHandler { } pub fn pop(&self) -> LoroResult> { - with_txn(&self.inner.txn, |txn| self.pop_with_txn(txn)) + match &self.inner { + MaybeDetached::Detached(l) => { + let mut list = l.try_lock().unwrap(); + Ok(list.value.pop().map(|v| v.to_value())) + } + MaybeDetached::Attached(a) => a.with_txn(|txn| self.pop_with_txn(txn)), + } } pub fn pop_with_txn(&self, txn: &mut Transaction) -> LoroResult> { @@ -779,18 +1575,26 @@ impl ListHandler { Ok(v) } - pub fn insert_container(&self, pos: usize, c_type: ContainerType) -> LoroResult { - with_txn(&self.inner.txn, |txn| { - self.insert_container_with_txn(txn, pos, c_type) - }) + pub fn insert_container(&self, pos: usize, child: H) -> LoroResult { + match &self.inner { + MaybeDetached::Detached(l) => { + let mut list = l.try_lock().unwrap(); + list.value + .insert(pos, ValueOrHandler::Handler(child.to_handler())); + Ok(child) + } + MaybeDetached::Attached(a) => { + a.with_txn(|txn| self.insert_container_with_txn(txn, pos, child)) + } + } } - pub fn insert_container_with_txn( + pub fn insert_container_with_txn( &self, txn: &mut Transaction, pos: usize, - c_type: ContainerType, - ) -> LoroResult { + child: H, + ) -> LoroResult { if pos > self.len() { return Err(LoroError::OutOfBound { pos, @@ -798,23 +1602,32 @@ impl ListHandler { }); } + let inner = self.inner.try_attached_state()?; let id = txn.next_id(); - let container_id = ContainerID::new_normal(id, c_type); + let container_id = ContainerID::new_normal(id, child.kind()); let v = LoroValue::Container(container_id.clone()); txn.apply_local_op( - self.inner.container_idx, + inner.container_idx, crate::op::RawOpContent::List(crate::container::list::list_op::ListOp::Insert { slice: ListSlice::RawData(Cow::Owned(vec![v.clone()])), pos, }), EventHint::InsertList { len: 1 }, - &self.inner.state, + &inner.state, )?; - Ok(create_handler(self, container_id)) + let ans = child.attach(txn, inner, container_id)?; + Ok(ans) } pub fn delete(&self, pos: usize, len: usize) -> LoroResult<()> { - with_txn(&self.inner.txn, |txn| self.delete_with_txn(txn, pos, len)) + match &self.inner { + MaybeDetached::Detached(l) => { + let mut list = l.try_lock().unwrap(); + list.value.drain(pos..pos + len); + Ok(()) + } + MaybeDetached::Attached(a) => a.with_txn(|txn| self.delete_with_txn(txn, pos, len)), + } } pub fn delete_with_txn(&self, txn: &mut Transaction, pos: usize, len: usize) -> LoroResult<()> { @@ -829,7 +1642,8 @@ impl ListHandler { }); } - let ids: Vec<_> = self.with_state(|state| { + let inner = self.inner.try_attached_state()?; + let ids: Vec<_> = inner.with_state(|state| { let list = state.as_list_state().unwrap(); (pos..pos + len) .map(|i| list.get_id_at(i).unwrap()) @@ -838,102 +1652,167 @@ impl ListHandler { for id in ids.into_iter() { txn.apply_local_op( - self.inner.container_idx, + inner.container_idx, crate::op::RawOpContent::List(ListOp::Delete(DeleteSpanWithId::new( id.id(), pos as isize, 1, ))), EventHint::DeleteList(DeleteSpan::new(pos as isize, 1)), - &self.inner.state, + &inner.state, )?; } Ok(()) } - pub fn get_child_handler(&self, index: usize) -> Handler { - let container_id = self.with_state(|state| { - state - .as_list_state() - .as_ref() - .unwrap() - .get(index) - .unwrap() - .as_container() - .unwrap() - .clone() - }); - create_handler(self, container_id) + pub fn get_child_handler(&self, index: usize) -> LoroResult { + match &self.inner { + MaybeDetached::Detached(l) => { + let list = l.try_lock().unwrap(); + let value = list.value.get(index).ok_or(LoroError::OutOfBound { + pos: index, + len: list.value.len(), + })?; + match value { + ValueOrHandler::Handler(h) => Ok(h.clone()), + _ => Err(LoroError::ArgErr( + format!( + "Expected container at index {}, but found {:?}", + index, value + ) + .into_boxed_str(), + )), + } + } + MaybeDetached::Attached(a) => { + let Some(value) = a.with_state(|state| { + state.as_list_state().as_ref().unwrap().get(index).cloned() + }) else { + return Err(LoroError::OutOfBound { + pos: index, + len: a.with_state(|state| state.as_list_state().unwrap().len()), + }); + }; + match value { + LoroValue::Container(id) => Ok(create_handler(a, id)), + _ => Err(LoroError::ArgErr( + format!( + "Expected container at index {}, but found {:?}", + index, value + ) + .into_boxed_str(), + )), + } + } + } } pub fn len(&self) -> usize { - self.with_state(|state| state.as_list_state().as_ref().unwrap().len()) + match &self.inner { + MaybeDetached::Detached(l) => l.try_lock().unwrap().value.len(), + MaybeDetached::Attached(a) => { + a.with_state(|state| state.as_list_state().unwrap().len()) + } + } } pub fn is_empty(&self) -> bool { self.len() == 0 } - pub fn get_deep_value_with_id(&self) -> LoroValue { - self.inner.with_doc_state(|state| { - state.get_container_deep_value_with_id(self.inner.container_idx, None) - }) + pub fn get_deep_value_with_id(&self) -> LoroResult { + let inner = self.inner.try_attached_state()?; + Ok(inner.with_doc_state(|state| { + state.get_container_deep_value_with_id(inner.container_idx, None) + })) } pub fn get(&self, index: usize) -> Option { - self.with_state(|state| { - let a = state.as_list_state().unwrap(); - a.get(index).cloned() - }) + match &self.inner { + MaybeDetached::Detached(l) => { + l.try_lock().unwrap().value.get(index).map(|x| x.to_value()) + } + MaybeDetached::Attached(a) => a.with_state(|state| { + let a = state.as_list_state().unwrap(); + a.get(index).cloned() + }), + } } /// Get value at given index, if it's a container, return a handler to the container pub fn get_(&self, index: usize) -> Option { - self.inner.with_doc_state(|doc_state| { - doc_state.with_state(self.inner.container_idx, |state| { - let a = state.as_list_state().unwrap(); - match a.get(index) { - Some(v) => { - if let LoroValue::Container(id) = v { - Some(ValueOrHandler::Handler(create_handler(self, id.clone()))) - } else { - Some(ValueOrHandler::Value(v.clone())) - } - } + match &self.inner { + MaybeDetached::Detached(l) => { + let l = l.try_lock().unwrap(); + l.value.get(index).cloned() + } + MaybeDetached::Attached(inner) => { + let value = + inner.with_state(|state| state.as_list_state().unwrap().get(index).cloned()); + match value { + Some(LoroValue::Container(container_id)) => Some(ValueOrHandler::Handler( + create_handler(inner, container_id.clone()), + )), + Some(value) => Some(ValueOrHandler::Value(value.clone())), None => None, } - }) - }) + } + } } pub fn for_each(&self, mut f: I) where I: FnMut(ValueOrHandler), { - self.inner.with_doc_state(|doc_state| { - doc_state.with_state(self.inner.container_idx, |state| { - let a = state.as_list_state().unwrap(); - for v in a.iter() { - match v { - LoroValue::Container(c) => { - f(ValueOrHandler::Handler(create_handler(self, c.clone()))); - } - value => { - f(ValueOrHandler::Value(value.clone())); + match &self.inner { + MaybeDetached::Detached(l) => { + let l = l.try_lock().unwrap(); + for v in l.value.iter() { + f(v.clone()) + } + } + MaybeDetached::Attached(inner) => { + inner.with_state(|state| { + let a = state.as_list_state().unwrap(); + for v in a.iter() { + match v { + LoroValue::Container(c) => { + f(ValueOrHandler::Handler(create_handler(inner, c.clone()))); + } + value => { + f(ValueOrHandler::Value(value.clone())); + } } } - } - }) - }) + }); + } + } } } impl MapHandler { + /// Create a new container that is detached from the document. + /// The edits on a detached container will not be persisted. + /// To attach the container to the document, please insert it into an attached container. + pub fn new_detached() -> Self { + Self { + inner: MaybeDetached::new_detached(Default::default()), + } + } + pub fn insert(&self, key: &str, value: impl Into) -> LoroResult<()> { - with_txn(&self.inner.txn, |txn| { - self.insert_with_txn(txn, key, value.into()) - }) + match &self.inner { + MaybeDetached::Detached(m) => { + let mut m = m.try_lock().unwrap(); + m.value + .insert(key.into(), ValueOrHandler::Value(value.into())); + Ok(()) + } + MaybeDetached::Attached(a) => { + a.with_txn(|txn| self.insert_with_txn(txn, key, value.into())) + } + } } pub fn insert_with_txn( @@ -942,9 +1821,12 @@ impl MapHandler { key: &str, value: LoroValue, ) -> LoroResult<()> { - if let Some(value) = value.as_container() { - self.insert_container_with_txn(txn, key, value.container_type())?; - return Ok(()); + if let Some(_value) = value.as_container() { + return Err(LoroError::ArgErr( + INSERT_CONTAINER_VALUE_ARG_ERROR + .to_string() + .into_boxed_str(), + )); } if self.get(key).map(|x| x == value).unwrap_or(false) { @@ -952,8 +1834,9 @@ impl MapHandler { return Ok(()); } + let inner = self.inner.try_attached_state()?; txn.apply_local_op( - self.inner.container_idx, + inner.container_idx, crate::op::RawOpContent::Map(crate::container::map::MapSet { key: key.into(), value: Some(value.clone()), @@ -962,26 +1845,40 @@ impl MapHandler { key: key.into(), value: Some(value.clone()), }, - &self.inner.state, + &inner.state, ) } - pub fn insert_container(&self, key: &str, c_type: ContainerType) -> LoroResult { - with_txn(&self.inner.txn, |txn| { - self.insert_container_with_txn(txn, key, c_type) - }) + pub fn insert_container(&self, key: &str, handler: T) -> LoroResult { + if handler.is_attached() { + return Err(LoroError::ReattachAttachedContainer); + } + + match &self.inner { + MaybeDetached::Detached(m) => { + let mut m = m.try_lock().unwrap(); + let to_insert = handler.to_handler(); + m.value + .insert(key.into(), ValueOrHandler::Handler(to_insert.clone())); + Ok(handler) + } + MaybeDetached::Attached(a) => { + a.with_txn(|txn| self.insert_container_with_txn(txn, key, handler)) + } + } } - pub fn insert_container_with_txn( + pub fn insert_container_with_txn( &self, txn: &mut Transaction, key: &str, - c_type: ContainerType, - ) -> LoroResult { + child: H, + ) -> LoroResult { + let inner = self.inner.try_attached_state()?; let id = txn.next_id(); - let container_id = ContainerID::new_normal(id, c_type); + let container_id = ContainerID::new_normal(id, child.kind()); txn.apply_local_op( - self.inner.container_idx, + inner.container_idx, crate::op::RawOpContent::Map(crate::container::map::MapSet { key: key.into(), value: Some(LoroValue::Container(container_id.clone())), @@ -990,19 +1887,27 @@ impl MapHandler { key: key.into(), value: Some(LoroValue::Container(container_id.clone())), }, - &self.inner.state, + &inner.state, )?; - Ok(create_handler(self, container_id)) + child.attach(txn, inner, container_id) } pub fn delete(&self, key: &str) -> LoroResult<()> { - with_txn(&self.inner.txn, |txn| self.delete_with_txn(txn, key)) + match &self.inner { + MaybeDetached::Detached(m) => { + let mut m = m.try_lock().unwrap(); + m.value.remove(key); + Ok(()) + } + MaybeDetached::Attached(a) => a.with_txn(|txn| self.delete_with_txn(txn, key)), + } } pub fn delete_with_txn(&self, txn: &mut Transaction, key: &str) -> LoroResult<()> { + let inner = self.inner.try_attached_state()?; txn.apply_local_op( - self.inner.container_idx, + inner.container_idx, crate::op::RawOpContent::Map(crate::container::map::MapSet { key: key.into(), value: None, @@ -1011,7 +1916,7 @@ impl MapHandler { key: key.into(), value: None, }, - &self.inner.state, + &inner.state, ) } @@ -1019,88 +1924,131 @@ impl MapHandler { where I: FnMut(&str, ValueOrHandler), { - let mutex = &self.inner.state.upgrade().unwrap(); - let mut doc_state = mutex.lock().unwrap(); - doc_state.with_state(self.inner.container_idx, |state| { - let a = state.as_map_state().unwrap(); - for (k, v) in a.iter() { - match &v.value { - Some(v) => match v { - LoroValue::Container(c) => { - f(k, ValueOrHandler::Handler(create_handler(self, c.clone()))) - } - value => f(k, ValueOrHandler::Value(value.clone())), - }, - None => {} + match &self.inner { + MaybeDetached::Detached(m) => { + let m = m.try_lock().unwrap(); + for (k, v) in m.value.iter() { + f(k, v.clone()); } } - }) + MaybeDetached::Attached(inner) => { + inner.with_state(|state| { + let a = state.as_map_state().unwrap(); + for (k, v) in a.iter() { + match &v.value { + Some(v) => match v { + LoroValue::Container(c) => { + f(k, ValueOrHandler::Handler(create_handler(inner, c.clone()))) + } + value => f(k, ValueOrHandler::Value(value.clone())), + }, + None => {} + } + } + }); + } + } } - pub fn get_child_handler(&self, key: &str) -> Handler { - let container_id = self.with_state(|state| { - state - .as_map_state() - .as_ref() - .unwrap() - .get(key) - .unwrap() - .as_container() - .unwrap() - .clone() - }); - create_handler(self, container_id) + pub fn get_child_handler(&self, key: &str) -> LoroResult { + match &self.inner { + MaybeDetached::Detached(m) => { + let m = m.try_lock().unwrap(); + let value = m.value.get(key).unwrap(); + match value { + ValueOrHandler::Value(v) => Err(LoroError::ArgErr( + format!("Expected Handler but found {:?}", v).into_boxed_str(), + )), + ValueOrHandler::Handler(h) => Ok(h.clone()), + } + } + MaybeDetached::Attached(inner) => { + let container_id = inner.with_state(|state| { + state + .as_map_state() + .as_ref() + .unwrap() + .get(key) + .unwrap() + .as_container() + .unwrap() + .clone() + }); + Ok(create_handler(inner, container_id)) + } + } } - pub fn get_deep_value_with_id(&self) -> LoroValue { - self.inner.with_doc_state(|state| { - state.get_container_deep_value_with_id(self.inner.container_idx, None) - }) + pub fn get_deep_value_with_id(&self) -> LoroResult { + match &self.inner { + MaybeDetached::Detached(_) => Err(LoroError::MisuseDettachedContainer { + method: "get_deep_value_with_id", + }), + MaybeDetached::Attached(inner) => Ok(inner.with_doc_state(|state| { + state.get_container_deep_value_with_id(inner.container_idx, None) + })), + } } pub fn get(&self, key: &str) -> Option { - self.with_state(|state| { - let a = state.as_map_state().unwrap(); - a.get(key).cloned() - }) + match &self.inner { + MaybeDetached::Detached(m) => { + let m = m.try_lock().unwrap(); + m.value.get(key).map(|v| v.to_value()) + } + MaybeDetached::Attached(inner) => { + inner.with_state(|state| state.as_map_state().unwrap().get(key).cloned()) + } + } } /// Get the value at given key, if value is a container, return a handler to the container pub fn get_(&self, key: &str) -> Option { - self.with_state(|state| { - let a = state.as_map_state().unwrap(); - let value = a.get(key); - match value { - Some(LoroValue::Container(container_id)) => Some(ValueOrHandler::Handler( - create_handler(self, container_id.clone()), - )), - Some(value) => Some(ValueOrHandler::Value(value.clone())), - None => None, + match &self.inner { + MaybeDetached::Detached(m) => { + let m = m.try_lock().unwrap(); + m.value.get(key).cloned() } - }) + MaybeDetached::Attached(inner) => { + let value = + inner.with_state(|state| state.as_map_state().unwrap().get(key).cloned()); + match value { + Some(LoroValue::Container(container_id)) => Some(ValueOrHandler::Handler( + create_handler(inner, container_id.clone()), + )), + Some(value) => Some(ValueOrHandler::Value(value.clone())), + None => None, + } + } + } } - pub fn get_or_create_container_( - &self, - key: &str, - container_type: ContainerType, - ) -> LoroResult { + pub fn get_or_create_container(&self, key: &str, child: C) -> LoroResult { if let Some(ans) = self.get_(key) { if let ValueOrHandler::Handler(h) = ans { - return Ok(h); + let kind = h.kind(); + return C::from_handler(h).ok_or_else(move || { + LoroError::ArgErr( + format!("Expected value type {} but found {:?}", child.kind(), kind) + .into_boxed_str(), + ) + }); } else { return Err(LoroError::ArgErr( - format!("Expected value type {} but found {:?}", container_type, ans) + format!("Expected value type {} but found {:?}", child.kind(), ans) .into_boxed_str(), )); } } - self.insert_container(key, container_type) + self.insert_container(key, child) } pub fn len(&self) -> usize { - self.with_state(|state| state.as_map_state().as_ref().unwrap().len()) + match &self.inner { + MaybeDetached::Detached(m) => m.try_lock().unwrap().value.len(), + MaybeDetached::Attached(a) => a.with_state(|state| state.as_map_state().unwrap().len()), + } } pub fn is_empty(&self) -> bool { @@ -1109,13 +2057,32 @@ impl MapHandler { } impl TreeHandler { + /// Create a new container that is detached from the document. + /// + /// The edits on a detached container will not be persisted/synced. + /// To attach the container to the document, please insert it into an attached + /// container. + pub fn new_detached() -> Self { + Self { + inner: MaybeDetached::new_detached(TreeInner::new()), + } + } + pub fn delete(&self, target: TreeID) -> LoroResult<()> { - with_txn(&self.inner.txn, |txn| self.delete_with_txn(txn, target)) + match &self.inner { + MaybeDetached::Detached(t) => { + let mut t = t.try_lock().unwrap(); + t.value.delete(target); + Ok(()) + } + MaybeDetached::Attached(a) => a.with_txn(|txn| self.delete_with_txn(txn, target)), + } } pub fn delete_with_txn(&self, txn: &mut Transaction, target: TreeID) -> LoroResult<()> { + let inner = self.inner.try_attached_state()?; txn.apply_local_op( - self.inner.container_idx, + inner.container_idx, crate::op::RawOpContent::Tree(TreeOp { target, parent: Some(TreeID::delete_root()), @@ -1124,12 +2091,18 @@ impl TreeHandler { target, action: TreeExternalDiff::Delete, }), - &self.inner.state, + &inner.state, ) } pub fn create>>(&self, parent: T) -> LoroResult { - with_txn(&self.inner.txn, |txn| self.create_with_txn(txn, parent)) + match &self.inner { + MaybeDetached::Detached(t) => { + let mut t = t.try_lock().unwrap(); + Ok(t.value.create(parent.into())) + } + MaybeDetached::Attached(a) => a.with_txn(|txn| self.create_with_txn(txn, parent)), + } } pub fn create_with_txn>>( @@ -1137,6 +2110,7 @@ impl TreeHandler { txn: &mut Transaction, parent: T, ) -> LoroResult { + let inner = self.inner.try_attached_state()?; let parent: Option = parent.into(); let tree_id = TreeID::from_id(txn.next_id()); let event_hint = TreeDiffItem { @@ -1144,21 +2118,26 @@ impl TreeHandler { action: TreeExternalDiff::Create(parent), }; txn.apply_local_op( - self.inner.container_idx, + inner.container_idx, crate::op::RawOpContent::Tree(TreeOp { target: tree_id, parent, }), EventHint::Tree(event_hint), - &self.inner.state, + &inner.state, )?; Ok(tree_id) } pub fn mov>>(&self, target: TreeID, parent: T) -> LoroResult<()> { - with_txn(&self.inner.txn, |txn| { - self.mov_with_txn(txn, target, parent) - }) + match &self.inner { + MaybeDetached::Detached(t) => { + let mut t = t.try_lock().unwrap(); + t.value.mov(target, parent.into()); + Ok(()) + } + MaybeDetached::Attached(a) => a.with_txn(|txn| self.mov_with_txn(txn, target, parent)), + } } pub fn mov_with_txn>>( @@ -1168,73 +2147,107 @@ impl TreeHandler { parent: T, ) -> LoroResult<()> { let parent = parent.into(); + let inner = self.inner.try_attached_state()?; txn.apply_local_op( - self.inner.container_idx, + inner.container_idx, crate::op::RawOpContent::Tree(TreeOp { target, parent }), EventHint::Tree(TreeDiffItem { target, action: TreeExternalDiff::Move(parent), }), - &self.inner.state, + &inner.state, ) } pub fn get_meta(&self, target: TreeID) -> LoroResult { - if !self.contains(target) { - return Err(LoroTreeError::TreeNodeNotExist(target).into()); + match &self.inner { + MaybeDetached::Detached(d) => { + let d = d.try_lock().unwrap(); + d.value + .map + .get(&target) + .cloned() + .ok_or(LoroTreeError::TreeNodeNotExist(target).into()) + } + MaybeDetached::Attached(a) => { + if !self.contains(target) { + return Err(LoroTreeError::TreeNodeNotExist(target).into()); + } + let map_container_id = target.associated_meta_container(); + let handler = create_handler(a, map_container_id); + Ok(handler.into_map().unwrap()) + } } - let map_container_id = target.associated_meta_container(); - let handler = create_handler(self, map_container_id); - Ok(handler.into_map().unwrap()) } /// 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> { - self.with_state(|state| { - let a = state.as_tree_state().unwrap(); - a.parent(target).map(|p| match p { - TreeParentId::None => None, - TreeParentId::Node(parent_id) => Some(parent_id), - _ => unreachable!(), - }) - }) + match &self.inner { + MaybeDetached::Detached(t) => { + let t = t.try_lock().unwrap(); + t.value.get_parent(target) + } + MaybeDetached::Attached(a) => a.with_state(|state| { + let a = state.as_tree_state().unwrap(); + a.parent(target).map(|p| match p { + TreeParentId::None => None, + TreeParentId::Node(parent_id) => Some(parent_id), + _ => unreachable!(), + }) + }), + } } pub fn children(&self, target: TreeID) -> Vec { - self.with_state(|state| { - let a = state.as_tree_state().unwrap(); - a.as_ref() - .get_children(&TreeParentId::Node(target)) - .into_iter() - .collect() - }) + match &self.inner { + MaybeDetached::Detached(t) => { + let t = t.try_lock().unwrap(); + t.value.get_children(target).unwrap() + } + MaybeDetached::Attached(a) => a.with_state(|state| { + let a = state.as_tree_state().unwrap(); + a.get_children(&TreeParentId::Node(target)) + }), + } } pub fn contains(&self, target: TreeID) -> bool { - self.with_state(|state| { - let a = state.as_tree_state().unwrap(); - a.contains(target) - }) + match &self.inner { + MaybeDetached::Detached(t) => { + let t = t.try_lock().unwrap(); + t.value.map.contains_key(&target) + } + MaybeDetached::Attached(a) => a.with_state(|state| { + let a = state.as_tree_state().unwrap(); + a.contains(target) + }), + } } pub fn nodes(&self) -> Vec { - self.with_state(|state| { - let a = state.as_tree_state().unwrap(); - a.nodes() - }) - } - - #[cfg(feature = "test_utils")] - pub fn max_counter(&self) -> i32 { - self.with_state(|state| { - let a = state.as_tree_state().unwrap(); - a.max_counter() - }) + match &self.inner { + MaybeDetached::Detached(t) => { + let t = t.try_lock().unwrap(); + t.value.map.keys().cloned().collect() + } + MaybeDetached::Attached(a) => a.with_state(|state| { + let a = state.as_tree_state().unwrap(); + a.nodes() + }), + } } #[cfg(feature = "test_utils")] pub fn next_tree_id(&self) -> TreeID { - with_txn(&self.inner.txn, |txn| Ok(TreeID::from_id(txn.next_id()))).unwrap() + match &self.inner { + MaybeDetached::Detached(d) => { + let d = d.try_lock().unwrap(); + TreeID::new(PeerID::MAX, d.value.next_counter) + } + MaybeDetached::Attached(a) => a + .with_txn(|txn| Ok(TreeID::from_id(txn.next_id()))) + .unwrap(), + } } } diff --git a/crates/loro-internal/src/loro.rs b/crates/loro-internal/src/loro.rs index 9dbe5752..b8fdf609 100644 --- a/crates/loro-internal/src/loro.rs +++ b/crates/loro-internal/src/loro.rs @@ -16,7 +16,7 @@ use crate::{ arena::SharedArena, change::Timestamp, configure::Configure, - container::{idx::ContainerIdx, richtext::config::StyleConfigMap, IntoContainerId}, + container::{richtext::config::StyleConfigMap, IntoContainerId}, dag::DagUtils, encoding::{ decode_snapshot, export_snapshot, parse_header_and_body, EncodeMode, ParsedHeaderAndBody, @@ -561,7 +561,7 @@ impl LoroDoc { #[inline] pub fn get_text(&self, id: I) -> TextHandler { let id = id.into_container_id(&self.arena, ContainerType::Text); - Handler::new( + Handler::new_attached( id, self.arena.clone(), self.get_global_txn(), @@ -576,7 +576,7 @@ impl LoroDoc { #[inline] pub fn get_list(&self, id: I) -> ListHandler { let id = id.into_container_id(&self.arena, ContainerType::List); - Handler::new( + Handler::new_attached( id, self.arena.clone(), self.get_global_txn(), @@ -591,7 +591,7 @@ impl LoroDoc { #[inline] pub fn get_map(&self, id: I) -> MapHandler { let id = id.into_container_id(&self.arena, ContainerType::Map); - Handler::new( + Handler::new_attached( id, self.arena.clone(), self.get_global_txn(), @@ -606,7 +606,7 @@ impl LoroDoc { #[inline] pub fn get_tree(&self, id: I) -> TreeHandler { let id = id.into_container_id(&self.arena, ContainerType::Tree); - Handler::new( + Handler::new_attached( id, self.arena.clone(), self.get_global_txn(), @@ -622,12 +622,6 @@ impl LoroDoc { self.oplog().lock().unwrap().diagnose_size(); } - #[inline] - fn get_container_idx(&self, id: I, c_type: ContainerType) -> ContainerIdx { - let id = id.into_container_id(&self.arena, c_type); - self.arena.register_container(&id) - } - #[inline] pub fn oplog_frontiers(&self) -> Frontiers { self.oplog().lock().unwrap().frontiers().clone() diff --git a/crates/loro-internal/src/oplog/dag.rs b/crates/loro-internal/src/oplog/dag.rs index cd19a4ce..74312131 100644 --- a/crates/loro-internal/src/oplog/dag.rs +++ b/crates/loro-internal/src/oplog/dag.rs @@ -166,12 +166,8 @@ impl AppDag { pub fn frontiers_to_vv(&self, frontiers: &Frontiers) -> Option { let mut vv: VersionVector = Default::default(); for id in frontiers.iter() { - let Some(rle) = self.map.get(&id.peer) else { - return None; - }; - let Some(x) = rle.get_by_atom_index(id.counter) else { - return None; - }; + let rle = self.map.get(&id.peer)?; + let x = rle.get_by_atom_index(id.counter)?; vv.extend_to_include_vv(x.element.vv.iter()); vv.extend_to_include_last_id(*id); } diff --git a/crates/loro-internal/src/state.rs b/crates/loro-internal/src/state.rs index 98ebb56d..5e223e8a 100644 --- a/crates/loro-internal/src/state.rs +++ b/crates/loro-internal/src/state.rs @@ -693,6 +693,7 @@ impl DocState { } #[inline(always)] + #[allow(unused)] pub(crate) fn with_state(&mut self, idx: ContainerIdx, f: F) -> R where F: FnOnce(&State) -> R, diff --git a/crates/loro-internal/src/txn.rs b/crates/loro-internal/src/txn.rs index 489438e7..d11161b9 100644 --- a/crates/loro-internal/src/txn.rs +++ b/crates/loro-internal/src/txn.rs @@ -379,7 +379,7 @@ impl Transaction { /// if it's str it will use Root container, which will not be None pub fn get_text(&self, id: I) -> TextHandler { let id = id.into_container_id(&self.arena, ContainerType::Text); - Handler::new( + Handler::new_attached( id, self.arena.clone(), self.global_txn.clone(), @@ -393,7 +393,7 @@ impl Transaction { /// if it's str it will use Root container, which will not be None pub fn get_list(&self, id: I) -> ListHandler { let id = id.into_container_id(&self.arena, ContainerType::List); - Handler::new( + Handler::new_attached( id, self.arena.clone(), self.global_txn.clone(), @@ -407,7 +407,7 @@ impl Transaction { /// if it's str it will use Root container, which will not be None pub fn get_map(&self, id: I) -> MapHandler { let id = id.into_container_id(&self.arena, ContainerType::Map); - Handler::new( + Handler::new_attached( id, self.arena.clone(), self.global_txn.clone(), @@ -421,7 +421,7 @@ impl Transaction { /// if it's str it will use Root container, which will not be None pub fn get_tree(&self, id: I) -> TreeHandler { let id = id.into_container_id(&self.arena, ContainerType::Tree); - Handler::new( + Handler::new_attached( id, self.arena.clone(), self.global_txn.clone(), @@ -431,11 +431,6 @@ impl Transaction { .unwrap() } - fn get_container_idx(&self, id: I, c_type: ContainerType) -> ContainerIdx { - let id = id.into_container_id(&self.arena, c_type); - self.arena.register_container(&id) - } - pub fn get_value_by_idx(&self, idx: ContainerIdx) -> LoroValue { self.state.lock().unwrap().get_value_by_idx(idx) } diff --git a/crates/loro-internal/tests/autocommit.rs b/crates/loro-internal/tests/autocommit.rs index 0616757d..31f9b56e 100644 --- a/crates/loro-internal/tests/autocommit.rs +++ b/crates/loro-internal/tests/autocommit.rs @@ -1,5 +1,5 @@ use loro_common::ID; -use loro_internal::{version::Frontiers, HandlerTrait, LoroDoc, ToJson}; +use loro_internal::{version::Frontiers, HandlerTrait, LoroDoc, TextHandler, ToJson}; use serde_json::json; #[test] @@ -32,9 +32,9 @@ fn auto_commit_list() { list_a.insert(0, "hello").unwrap(); assert_eq!(list_a.get_value().to_json_value(), json!(["hello"])); let text_a = list_a - .insert_container(0, loro_common::ContainerType::Text) + .insert_container(0, TextHandler::new_detached()) .unwrap(); - let text = text_a.into_text().unwrap(); + let text = text_a; text.insert(0, "world").unwrap(); let value = doc_a.get_deep_value(); assert_eq!(value.to_json_value(), json!({"list": ["world", "hello"]})) diff --git a/crates/loro-internal/tests/test.rs b/crates/loro-internal/tests/test.rs index bdf96687..d8221e65 100644 --- a/crates/loro-internal/tests/test.rs +++ b/crates/loro-internal/tests/test.rs @@ -7,7 +7,7 @@ use loro_internal::{ event::Diff, handler::{Handler, TextDelta, ValueOrHandler}, version::Frontiers, - ApplyDiff, HandlerTrait, LoroDoc, ToJson, + ApplyDiff, HandlerTrait, ListHandler, LoroDoc, MapHandler, TextHandler, ToJson, }; use serde_json::json; @@ -132,7 +132,8 @@ fn handler_in_event() { assert!(matches!(value, ValueOrHandler::Handler(Handler::Text(_)))); })); let list = doc.get_list("list"); - list.insert_container(0, ContainerType::Text).unwrap(); + list.insert_container(0, TextHandler::new_detached()) + .unwrap(); doc.commit_then_renew(); } @@ -156,7 +157,7 @@ fn out_of_bound_test() { assert!(matches!(err, loro_common::LoroError::OutOfBound { .. })); let err = a .get_list("list") - .insert_container(3, ContainerType::Map) + .insert_container(3, MapHandler::new_detached()) .unwrap_err(); assert!(matches!(err, loro_common::LoroError::OutOfBound { .. })); } @@ -168,22 +169,18 @@ fn list() { assert_eq!(a.get_list("list").get(0).unwrap(), LoroValue::from("Hello")); let map = a .get_list("list") - .insert_container(1, ContainerType::Map) - .unwrap() - .into_map() + .insert_container(1, MapHandler::new_detached()) .unwrap(); map.insert("Hello", LoroValue::from("u")).unwrap(); let pos = map - .insert_container("pos", ContainerType::Map) - .unwrap() - .into_map() + .insert_container("pos", MapHandler::new_detached()) .unwrap(); pos.insert("x", 0).unwrap(); pos.insert("y", 100).unwrap(); let cid = map.id(); let id = a.get_list("list").get(1); - assert_eq!(id.as_ref().unwrap().as_container().unwrap(), cid); + assert_eq!(id.as_ref().unwrap().as_container().unwrap(), &cid); let map = a.get_map(id.unwrap().into_container().unwrap()); let new_pos = a.get_map(map.get("pos").unwrap().into_container().unwrap()); assert_eq!( @@ -220,7 +217,7 @@ fn richtext_mark_event() { a.commit_then_stop(); let b = LoroDoc::new_auto_commit(); b.subscribe( - a.get_text("text").id(), + &a.get_text("text").id(), Arc::new(|e| { let delta = e.events[0].diff.as_text().unwrap(); assert_eq!( @@ -441,8 +438,9 @@ fn test_checkout() { let map = doc_0.get_map("map"); doc_0 .with_txn(|txn| { - let handler = map.insert_container_with_txn(txn, "text", ContainerType::Text)?; - let text = handler.into_text().unwrap(); + let handler = + map.insert_container_with_txn(txn, "text", TextHandler::new_detached())?; + let text = handler; text.insert_with_txn(txn, 0, "123") }) .unwrap(); @@ -598,14 +596,8 @@ fn a_list_of_map_checkout() { let entry = doc.get_map("entry"); let (list, sub) = doc .with_txn(|txn| { - let list = entry - .insert_container_with_txn(txn, "list", loro_common::ContainerType::List)? - .into_list() - .unwrap(); - let sub_map = list - .insert_container_with_txn(txn, 0, loro_common::ContainerType::Map)? - .into_map() - .unwrap(); + let list = entry.insert_container_with_txn(txn, "list", ListHandler::new_detached())?; + let sub_map = list.insert_container_with_txn(txn, 0, MapHandler::new_detached())?; sub_map.insert_with_txn(txn, "x", 100.into())?; sub_map.insert_with_txn(txn, "y", 1000.into())?; Ok((list, sub_map)) @@ -616,8 +608,8 @@ fn a_list_of_map_checkout() { doc.with_txn(|txn| { list.insert_with_txn(txn, 0, 3.into())?; list.push_with_txn(txn, 4.into())?; - list.insert_container_with_txn(txn, 2, loro_common::ContainerType::Map)?; - list.insert_container_with_txn(txn, 3, loro_common::ContainerType::Map)?; + list.insert_container_with_txn(txn, 2, MapHandler::new_detached())?; + list.insert_container_with_txn(txn, 3, TextHandler::new_detached())?; Ok(()) }) .unwrap(); diff --git a/crates/loro-wasm/src/convert.rs b/crates/loro-wasm/src/convert.rs index b2e80f13..aaeb9d8e 100644 --- a/crates/loro-wasm/src/convert.rs +++ b/crates/loro-wasm/src/convert.rs @@ -7,61 +7,58 @@ use loro_internal::handler::{Handler, ValueOrHandler}; use loro_internal::{LoroDoc, LoroValue}; use wasm_bindgen::JsValue; -use crate::{LoroList, LoroMap, LoroText, LoroTree}; +use crate::{Container, JsContainer, LoroList, LoroMap, LoroText, LoroTree}; use wasm_bindgen::__rt::IntoJsResult; -use wasm_bindgen::convert::FromWasmAbi; +use wasm_bindgen::convert::RefFromWasmAbi; /// Convert a `JsValue` to `T` by constructor's name. /// /// more details can be found in https://github.com/rustwasm/wasm-bindgen/issues/2231#issuecomment-656293288 -pub(crate) fn js_to_any>( - js: JsValue, - struct_name: &str, -) -> Result { +pub(crate) fn js_to_container(js: JsContainer) -> Result { + let js: JsValue = js.into(); if !js.is_object() { - return Err(JsValue::from_str( - format!("Value supplied as {} is not an object", struct_name).as_str(), - )); + return Err(JsValue::from_str(&format!( + "Value supplied is not an object, but {:?}", + js + ))); } let ctor_name = Object::get_prototype_of(&js).constructor().name(); - if ctor_name == struct_name { - let ptr = Reflect::get(&js, &JsValue::from_str("ptr"))?; - let ptr_u32: u32 = ptr.as_f64().ok_or(JsValue::NULL)? as u32; - let obj = unsafe { T::from_abi(ptr_u32) }; - Ok(obj) - } else { - return Err(JsValue::from_str( - format!( - "Value ctor_name is {} but the required struct name is {}", - ctor_name, struct_name - ) - .as_str(), - )); - } -} + let Ok(ptr) = Reflect::get(&js, &JsValue::from_str("__wbg_ptr")) else { + return Err(JsValue::from_str("Cannot find pointer field")); + }; + let ptr_u32: u32 = ptr.as_f64().unwrap() as u32; + let ctor_name = ctor_name + .as_string() + .ok_or(JsValue::from_str("Constructor name is not a string"))?; + let container = match ctor_name.as_str() { + "LoroText" => { + let obj = unsafe { LoroText::ref_from_abi(ptr_u32) }; + Container::Text(obj.clone()) + } + "LoroMap" => { + let obj = unsafe { LoroMap::ref_from_abi(ptr_u32) }; + Container::Map(obj.clone()) + } + "LoroList" => { + let obj = unsafe { LoroList::ref_from_abi(ptr_u32) }; + Container::List(obj.clone()) + } + "LoroTree" => { + let obj = unsafe { LoroTree::ref_from_abi(ptr_u32) }; + Container::Tree(obj.clone()) + } + _ => { + return Err(JsValue::from_str( + format!( + "Value ctor_name is {} but the valid container name is LoroMap, LoroList, LoroText or LoroTree", + ctor_name + ) + .as_str(), + )); + } + }; -impl TryFrom for LoroText { - type Error = JsValue; - - fn try_from(value: JsValue) -> Result { - js_to_any(value, "LoroText") - } -} - -impl TryFrom for LoroList { - type Error = JsValue; - - fn try_from(value: JsValue) -> Result { - js_to_any(value, "LoroList") - } -} - -impl TryFrom for LoroMap { - type Error = JsValue; - - fn try_from(value: JsValue) -> Result { - js_to_any(value, "LoroMap") - } + Ok(container) } pub(crate) fn resolved_diff_to_js(value: &Diff, doc: &Arc) -> JsValue { @@ -130,7 +127,7 @@ fn delta_item_to_js(item: DeltaItem, ()>, doc: &Arc for (i, v) in value.into_iter().enumerate() { let value = match v { ValueOrHandler::Value(v) => convert(v), - ValueOrHandler::Handler(h) => handler_to_js_value(h, doc.clone()), + ValueOrHandler::Handler(h) => handler_to_js_value(h, Some(doc.clone())), }; arr.set(i as u32, value); } @@ -198,7 +195,7 @@ fn map_delta_to_js(value: &ResolvedMapDelta, doc: &Arc) -> JsValue { let value = if let Some(value) = value.value.clone() { match value { ValueOrHandler::Value(v) => convert(v), - ValueOrHandler::Handler(h) => handler_to_js_value(h, doc.clone()), + ValueOrHandler::Handler(h) => handler_to_js_value(h, Some(doc.clone())), } } else { JsValue::null() @@ -210,7 +207,7 @@ fn map_delta_to_js(value: &ResolvedMapDelta, doc: &Arc) -> JsValue { obj.into_js_result().unwrap() } -pub(crate) fn handler_to_js_value(handler: Handler, doc: Arc) -> JsValue { +pub(crate) fn handler_to_js_value(handler: Handler, doc: Option>) -> JsValue { match handler { Handler::Text(t) => LoroText { handler: t, doc }.into(), Handler::Map(m) => LoroMap { handler: m, doc }.into(), diff --git a/crates/loro-wasm/src/lib.rs b/crates/loro-wasm/src/lib.rs index c7aa604f..e09d2b56 100644 --- a/crates/loro-wasm/src/lib.rs +++ b/crates/loro-wasm/src/lib.rs @@ -21,13 +21,12 @@ use std::{cell::RefCell, cmp::Ordering, panic, rc::Rc, sync::Arc}; use wasm_bindgen::{__rt::IntoJsResult, prelude::*}; mod log; -use crate::convert::handler_to_js_value; +use crate::convert::{handler_to_js_value, js_to_container}; mod convert; #[wasm_bindgen(start)] fn run() { - #[cfg(feature = "console_error_panic_hook")] console_error_panic_hook::set_once(); } @@ -91,12 +90,22 @@ extern "C" { typescript_type = "Map | Uint8Array | VersionVector | undefined | null" )] pub type JsIntoVersionVector; + #[wasm_bindgen(typescript_type = "Container")] + pub type JsContainer; #[wasm_bindgen(typescript_type = "Value | Container")] pub type JsValueOrContainer; #[wasm_bindgen(typescript_type = "Value | Container | undefined")] pub type JsValueOrContainerOrUndefined; #[wasm_bindgen(typescript_type = "Container | undefined")] pub type JsContainerOrUndefined; + #[wasm_bindgen(typescript_type = "LoroText | undefined")] + pub type JsLoroTextOrUndefined; + #[wasm_bindgen(typescript_type = "LoroMap | undefined")] + pub type JsLoroMapOrUndefined; + #[wasm_bindgen(typescript_type = "LoroList | undefined")] + pub type JsLoroListOrUndefined; + #[wasm_bindgen(typescript_type = "LoroTree | undefined")] + pub type JsLoroTreeOrUndefined; #[wasm_bindgen(typescript_type = "[string, Value | Container]")] pub type MapEntry; #[wasm_bindgen(typescript_type = "{[key: string]: { expand: 'before'|'after'|'none'|'both' }}")] @@ -496,7 +505,7 @@ impl Loro { .get_text(js_value_to_container_id(cid, ContainerType::Text)?); Ok(LoroText { handler: text, - doc: self.0.clone(), + doc: Some(self.0.clone()), }) } @@ -516,7 +525,7 @@ impl Loro { .get_map(js_value_to_container_id(cid, ContainerType::Map)?); Ok(LoroMap { handler: map, - doc: self.0.clone(), + doc: Some(self.0.clone()), }) } @@ -536,7 +545,7 @@ impl Loro { .get_list(js_value_to_container_id(cid, ContainerType::List)?); Ok(LoroList { handler: list, - doc: self.0.clone(), + doc: Some(self.0.clone()), }) } @@ -556,7 +565,7 @@ impl Loro { .get_tree(js_value_to_container_id(cid, ContainerType::Tree)?); Ok(LoroTree { handler: tree, - doc: self.0.clone(), + doc: Some(self.0.clone()), }) } @@ -581,7 +590,7 @@ impl Loro { let map = self.0.get_map(container_id); LoroMap { handler: map, - doc: self.0.clone(), + doc: Some(self.0.clone()), } .into() } @@ -589,7 +598,7 @@ impl Loro { let list = self.0.get_list(container_id); LoroList { handler: list, - doc: self.0.clone(), + doc: Some(self.0.clone()), } .into() } @@ -597,7 +606,7 @@ impl Loro { let richtext = self.0.get_text(container_id); LoroText { handler: richtext, - doc: self.0.clone(), + doc: Some(self.0.clone()), } .into() } @@ -605,7 +614,7 @@ impl Loro { let tree = self.0.get_tree(container_id); LoroTree { handler: tree, - doc: self.0.clone(), + doc: Some(self.0.clone()), } .into() } @@ -801,9 +810,9 @@ impl Loro { /// const doc = new Loro(); /// const list = doc.getList("list"); /// list.insert(0, "Hello"); - /// const text = list.insertContainer(0, "Text"); + /// const text = list.insertContainer(0, new LoroText()); /// text.insert(0, "Hello"); - /// const map = list.insertContainer(1, "Map"); + /// const map = list.insertContainer(1, new LoroMap()); /// map.set("foo", "bar"); /// /* /// {"list": ["Hello", {"foo": "bar"}]} @@ -1145,10 +1154,12 @@ fn convert_container_path_to_js_value(path: &[(ContainerID, Index)]) -> JsValue } /// The handler of a text or richtext container. +/// +#[derive(Clone)] #[wasm_bindgen] pub struct LoroText { handler: TextHandler, - doc: Arc, + doc: Option>, } #[derive(Serialize, Deserialize)] @@ -1159,6 +1170,18 @@ struct MarkRange { #[wasm_bindgen] impl LoroText { + /// Create a new detached LoroText. + /// + /// The edits on a detached container will not be persisted. + /// To attach the container to the document, please insert it into an attached container. + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self { + handler: TextHandler::new_detached(), + doc: None, + } + } + /// "Text" pub fn kind(&self) -> JsValue { JsValue::from_str("Text") @@ -1278,7 +1301,7 @@ impl LoroText { /// Get the container id of the text. #[wasm_bindgen(js_name = "id", method, getter)] pub fn id(&self) -> JsContainerID { - let value: JsValue = self.handler.id().into(); + let value: JsValue = (&self.handler.id()).into(); value.into() } @@ -1351,17 +1374,56 @@ impl LoroText { JsContainerOrUndefined::from(JsValue::UNDEFINED) } } + + /// Whether the container is attached to a docuemnt. + /// + /// If it's detached, the operations on the container will not be persisted. + #[wasm_bindgen(js_name = "isAttached")] + pub fn is_attached(&self) -> bool { + self.handler.is_attached() + } + + /// Get the attached container associated with this. + /// + /// Returns an attached `Container` that equals to this or created by this, otherwise `undefined`. + #[wasm_bindgen(js_name = "getAttached")] + pub fn get_attached(&self) -> JsLoroTextOrUndefined { + if let Some(h) = self.handler.get_attached() { + handler_to_js_value(Handler::Text(h), self.doc.clone()).into() + } else { + JsValue::UNDEFINED.into() + } + } +} + +impl Default for LoroText { + fn default() -> Self { + Self::new() + } } /// The handler of a map container. +#[derive(Clone)] #[wasm_bindgen] pub struct LoroMap { handler: MapHandler, - doc: Arc, + doc: Option>, } #[wasm_bindgen] impl LoroMap { + /// Create a new detached LoroMap. + /// + /// The edits on a detached container will not be persisted. + /// To attach the container to the document, please insert it into an attached container. + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self { + handler: MapHandler::new_detached(), + doc: None, + } + } + /// "Map" pub fn kind(&self) -> JsValue { JsValue::from_str("Map") @@ -1437,14 +1499,12 @@ impl LoroMap { /// const bar = map.get("foo"); /// ``` #[wasm_bindgen(js_name = "getOrCreateContainer")] - pub fn get_or_create_container( - &self, - key: &str, - container_type: &str, - ) -> JsResult { - let type_: ContainerType = container_type.try_into()?; - let v = self.handler.get_or_create_container_(key, type_)?; - Ok(handler_to_js_value(v, self.doc.clone()).into()) + pub fn get_or_create_container(&self, key: &str, child: JsContainer) -> JsResult { + let child = convert::js_to_container(child)?; + let handler = self + .handler + .get_or_create_container(key, child.to_handler())?; + Ok(handler_to_js_value(handler, self.doc.clone()).into()) } /// Get the keys of the map. @@ -1483,7 +1543,7 @@ impl LoroMap { pub fn values(&self) -> Vec { let mut ans: Vec = Vec::with_capacity(self.handler.len()); self.handler.for_each(|_, v| { - ans.push(loro_value_to_js_value_or_container(v, &self.doc)); + ans.push(loro_value_to_js_value_or_container(v, self.doc.clone())); }); ans } @@ -1506,7 +1566,7 @@ impl LoroMap { self.handler.for_each(|k, v| { let array = Array::new(); array.push(&k.to_string().into()); - array.push(&loro_value_to_js_value_or_container(v, &self.doc)); + array.push(&loro_value_to_js_value_or_container(v, self.doc.clone())); let v: JsValue = array.into(); ans.push(v.into()); }); @@ -1516,7 +1576,7 @@ impl LoroMap { /// The container id of this handler. #[wasm_bindgen(js_name = "id", method, getter)] pub fn id(&self) -> JsContainerID { - let value: JsValue = self.handler.id().into(); + let value: JsValue = (&self.handler.id()).into(); value.into() } @@ -1530,7 +1590,7 @@ impl LoroMap { /// const doc = new Loro(); /// const map = doc.getMap("map"); /// map.set("foo", "bar"); - /// const text = map.setContainer("text", "Text"); + /// const text = map.setContainer("text", new LoroText()); /// text.insert(0, "Hello"); /// console.log(map.getDeepValue()); // {"foo": "bar", "text": "Hello"} /// ``` @@ -1548,14 +1608,14 @@ impl LoroMap { /// const doc = new Loro(); /// const map = doc.getMap("map"); /// map.set("foo", "bar"); - /// const text = map.setContainer("text", "Text"); - /// const list = map.setContainer("list", "List"); + /// const text = map.setContainer("text", new LoroText()); + /// const list = map.setContainer("list", new LoroText()); /// ``` #[wasm_bindgen(js_name = "setContainer")] - pub fn insert_container(&mut self, key: &str, container_type: &str) -> JsResult { - let type_: ContainerType = container_type.try_into()?; - let c = self.handler.insert_container(key, type_)?; - Ok(handler_to_js_value(c, self.doc.clone())) + pub fn insert_container(&mut self, key: &str, child: JsContainer) -> JsResult { + let child = convert::js_to_container(child)?; + let c = self.handler.insert_container(key, child.to_handler())?; + Ok(handler_to_js_value(c, self.doc.clone()).into()) } /// Subscribe to the changes of the map. @@ -1633,17 +1693,55 @@ impl LoroMap { JsContainerOrUndefined::from(JsValue::UNDEFINED) } } + + /// Whether the container is attached to a docuemnt. + /// + /// If it's detached, the operations on the container will not be persisted. + #[wasm_bindgen(js_name = "isAttached")] + pub fn is_attached(&self) -> bool { + self.handler.is_attached() + } + + /// Get the attached container associated with this. + /// + /// Returns an attached `Container` that equals to this or created by this, otherwise `undefined`. + #[wasm_bindgen(js_name = "getAttached")] + pub fn get_attached(&self) -> JsLoroMapOrUndefined { + let Some(h) = self.handler.get_attached() else { + return JsValue::UNDEFINED.into(); + }; + handler_to_js_value(Handler::Map(h), self.doc.clone()).into() + } +} + +impl Default for LoroMap { + fn default() -> Self { + Self::new() + } } /// The handler of a list container. +#[derive(Clone)] #[wasm_bindgen] pub struct LoroList { handler: ListHandler, - doc: Arc, + doc: Option>, } #[wasm_bindgen] impl LoroList { + /// Create a new detached LoroList. + /// + /// The edits on a detached container will not be persisted. + /// To attach the container to the document, please insert it into an attached container. + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self { + handler: ListHandler::new_detached(), + doc: None, + } + } + /// "List" pub fn kind(&self) -> JsValue { JsValue::from_str("List") @@ -1711,7 +1809,7 @@ impl LoroList { /// Get the id of this container. #[wasm_bindgen(js_name = "id", method, getter)] pub fn id(&self) -> JsContainerID { - let value: JsValue = self.handler.id().into(); + let value: JsValue = (&self.handler.id()).into(); value.into() } @@ -1727,7 +1825,7 @@ impl LoroList { /// list.insert(0, 100); /// list.insert(1, "foo"); /// list.insert(2, true); - /// list.insertContainer(3, "Text"); + /// list.insertContainer(3, new LoroText()); /// console.log(list.value); // [100, "foo", true, LoroText]; /// ``` #[wasm_bindgen(js_name = "toArray", method)] @@ -1758,7 +1856,7 @@ impl LoroList { /// const doc = new Loro(); /// const list = doc.getList("list"); /// list.insert(0, 100); - /// const text = list.insertContainer(1, "Text"); + /// const text = list.insertContainer(1, new LoroText()); /// text.insert(0, "Hello"); /// console.log(list.getDeepValue()); // [100, "Hello"]; /// ``` @@ -1777,15 +1875,15 @@ impl LoroList { /// const doc = new Loro(); /// const list = doc.getList("list"); /// list.insert(0, 100); - /// const text = list.insertContainer(1, "Text"); + /// const text = list.insertContainer(1, new LoroText()); /// text.insert(0, "Hello"); /// console.log(list.getDeepValue()); // [100, "Hello"]; /// ``` #[wasm_bindgen(js_name = "insertContainer")] - pub fn insert_container(&mut self, index: usize, container: &str) -> JsResult { - let type_: ContainerType = container.try_into()?; - let c = self.handler.insert_container(index, type_)?; - Ok(handler_to_js_value(c, self.doc.clone())) + pub fn insert_container(&mut self, index: usize, child: JsContainer) -> JsResult { + let child = js_to_container(child)?; + let c = self.handler.insert_container(index, child.to_handler())?; + Ok(handler_to_js_value(c, self.doc.clone()).into()) } /// Subscribe to the changes of the list. @@ -1862,25 +1960,52 @@ impl LoroList { JsContainerOrUndefined::from(JsValue::UNDEFINED) } } + + /// Whether the container is attached to a docuemnt. + /// + /// If it's detached, the operations on the container will not be persisted. + #[wasm_bindgen(js_name = "isAttached")] + pub fn is_attached(&self) -> bool { + self.handler.is_attached() + } + + /// Get the attached container associated with this. + /// + /// Returns an attached `Container` that equals to this or created by this, otherwise `undefined`. + #[wasm_bindgen(js_name = "getAttached")] + pub fn get_attached(&self) -> JsLoroListOrUndefined { + if let Some(h) = self.handler.get_attached() { + handler_to_js_value(Handler::List(h), self.doc.clone()).into() + } else { + JsValue::UNDEFINED.into() + } + } +} + +impl Default for LoroList { + fn default() -> Self { + Self::new() + } } /// The handler of a tree(forest) container. +#[derive(Clone)] #[wasm_bindgen] pub struct LoroTree { handler: TreeHandler, - doc: Arc, + doc: Option>, } #[wasm_bindgen] pub struct LoroTreeNode { id: TreeID, tree: TreeHandler, - doc: Arc, + doc: Option>, } #[wasm_bindgen] impl LoroTreeNode { - fn from_tree(id: TreeID, tree: TreeHandler, doc: Arc) -> Self { + fn from_tree(id: TreeID, tree: TreeHandler, doc: Option>) -> Self { Self { id, tree, doc } } @@ -1965,6 +2090,18 @@ impl LoroTreeNode { #[wasm_bindgen] impl LoroTree { + /// Create a new detached LoroTree. + /// + /// The edits on a detached container will not be persisted. + /// To attach the container to the document, please insert it into an attached container. + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self { + handler: TreeHandler::new_detached(), + doc: None, + } + } + /// "Tree" pub fn kind(&self) -> JsValue { JsValue::from_str("Tree") @@ -2089,7 +2226,7 @@ impl LoroTree { /// Get the id of the container. #[wasm_bindgen(js_name = "id", method, getter)] pub fn id(&self) -> JsContainerID { - let value: JsValue = self.handler.id().into(); + let value: JsValue = (&self.handler.id()).into(); value.into() } @@ -2225,9 +2362,38 @@ impl LoroTree { JsContainerOrUndefined::from(JsValue::UNDEFINED) } } + + /// Whether the container is attached to a docuemnt. + /// + /// If it's detached, the operations on the container will not be persisted. + #[wasm_bindgen(js_name = "isAttached")] + pub fn is_attached(&self) -> bool { + self.handler.is_attached() + } + + /// Get the attached container associated with this. + /// + /// Returns an attached `Container` that equals to this or created by this, otherwise `undefined`. + #[wasm_bindgen(js_name = "getAttached")] + pub fn get_attached(&self) -> JsLoroTreeOrUndefined { + if let Some(h) = self.handler.get_attached() { + handler_to_js_value(Handler::Tree(h), self.doc.clone()).into() + } else { + JsValue::UNDEFINED.into() + } + } } -fn loro_value_to_js_value_or_container(value: ValueOrHandler, doc: &Arc) -> JsValue { +impl Default for LoroTree { + fn default() -> Self { + Self::new() + } +} + +fn loro_value_to_js_value_or_container( + value: ValueOrHandler, + doc: Option>, +) -> JsValue { match value { ValueOrHandler::Value(v) => { let value: JsValue = v.into(); @@ -2339,6 +2505,24 @@ fn id_value_to_u64(value: JsValue) -> JsResult { } } +pub enum Container { + Text(LoroText), + Map(LoroMap), + List(LoroList), + Tree(LoroTree), +} + +impl Container { + fn to_handler(&self) -> Handler { + match self { + Container::Text(t) => Handler::Text(t.handler.clone()), + Container::Map(m) => Handler::Map(m.handler.clone()), + Container::List(l) => Handler::List(l.handler.clone()), + Container::Tree(t) => Handler::Tree(t.handler.clone()), + } + } +} + #[wasm_bindgen(typescript_custom_section)] const TYPES: &'static str = r#" /** diff --git a/crates/loro/README.md b/crates/loro/README.md index 104d1bba..7e3cceb4 100644 --- a/crates/loro/README.md +++ b/crates/loro/README.md @@ -11,7 +11,7 @@ PS: Version control is forthcoming. Time travel functionality is already accessi ## Map/List/Text ```rust -use loro::{LoroDoc, ToJson, LoroValue}; +use loro::{LoroDoc, LoroList, LoroText, LoroValue, ToJson}; use serde_json::json; let doc = LoroDoc::new(); @@ -21,16 +21,10 @@ map.insert("true", true).unwrap(); map.insert("null", LoroValue::Null).unwrap(); map.insert("deleted", LoroValue::Null).unwrap(); map.delete("deleted").unwrap(); -let list = map - .insert_container("list", loro_internal::ContainerType::List).unwrap() - .into_list() - .unwrap(); -list.insert(0, "List"); -list.insert(1, 9); -let text = map - .insert_container("text", loro_internal::ContainerType::Text).unwrap() - .into_text() - .unwrap(); +let list = map.insert_container("list", LoroList::new()).unwrap(); +list.insert(0, "List").unwrap(); +list.insert(1, 9).unwrap(); +let text = map.insert_container("text", LoroText::new()).unwrap(); text.insert(0, "Hello world!").unwrap(); assert_eq!( doc.get_deep_value().to_json_value(), diff --git a/crates/loro/src/lib.rs b/crates/loro/src/lib.rs index f9a247b2..40f51f25 100644 --- a/crates/loro/src/lib.rs +++ b/crates/loro/src/lib.rs @@ -355,6 +355,25 @@ impl LoroDoc { } } +/// It's used to prevent the user from implementing the trait directly. +#[allow(private_bounds)] +trait SealedTrait {} +#[allow(private_bounds)] +pub trait ContainerTrait: SealedTrait { + type Handler: HandlerTrait; + fn to_container(&self) -> Container; + fn to_handler(&self) -> Self::Handler; + fn from_handler(handler: Self::Handler) -> Self; + fn try_from_container(container: Container) -> Option + where + Self: Sized; + fn is_attached(&self) -> bool; + /// If a detached container is attached, this method will return its corresponding attached handler. + fn get_attached(&self) -> Option + where + Self: Sized; +} + /// LoroList container. It's used to model array. /// /// It can have sub containers. @@ -378,7 +397,53 @@ pub struct LoroList { handler: InnerListHandler, } +impl SealedTrait for LoroList {} +impl ContainerTrait for LoroList { + type Handler = InnerListHandler; + fn to_container(&self) -> Container { + Container::List(self.clone()) + } + + fn to_handler(&self) -> Self::Handler { + self.handler.clone() + } + + fn from_handler(handler: Self::Handler) -> Self { + Self { handler } + } + + fn is_attached(&self) -> bool { + self.handler.is_attached() + } + + fn get_attached(&self) -> Option { + self.handler.get_attached().map(Self::from_handler) + } + + fn try_from_container(container: Container) -> Option { + container.into_list().ok() + } +} + impl LoroList { + /// Create a new container that is detached from the document. + /// + /// The edits on a detached container will not be persisted. + /// To attach the container to the document, please insert it into an attached container. + pub fn new() -> Self { + Self { + handler: InnerListHandler::new_detached(), + } + } + + /// Whether the container is attached to a document + /// + /// The edits on a detached container will not be persisted. + /// To attach the container to the document, please insert it into an attached container. + pub fn is_attached(&self) -> bool { + self.handler.is_attached() + } + pub fn insert(&self, pos: usize, v: impl Into) -> LoroResult<()> { self.handler.insert(pos, v) } @@ -423,9 +488,11 @@ impl LoroList { } #[inline] - pub fn push_container(&self, c_type: ContainerType) -> LoroResult { + pub fn push_container(&self, child: C) -> LoroResult { let pos = self.handler.len(); - Ok(Container::from(self.handler.insert_container(pos, c_type)?)) + Ok(C::from_handler( + self.handler.insert_container(pos, child.to_handler())?, + )) } pub fn for_each(&self, f: I) @@ -450,18 +517,26 @@ impl LoroList { /// # Example /// /// ``` - /// # use loro::{LoroDoc, ContainerType, ToJson}; + /// # use loro::{LoroDoc, ContainerType, LoroText, ToJson}; /// # use serde_json::json; /// let doc = LoroDoc::new(); /// let list = doc.get_list("m"); - /// let text = list.insert_container(0, ContainerType::Text).unwrap().into_text().unwrap(); + /// let text = list.insert_container(0, LoroText::new()).unwrap(); /// text.insert(0, "12"); /// text.insert(0, "0"); /// assert_eq!(doc.get_deep_value().to_json_value(), json!({"m": ["012"]})); /// ``` #[inline] - pub fn insert_container(&self, pos: usize, c_type: ContainerType) -> LoroResult { - Ok(Container::from(self.handler.insert_container(pos, c_type)?)) + pub fn insert_container(&self, pos: usize, child: C) -> LoroResult { + Ok(C::from_handler( + self.handler.insert_container(pos, child.to_handler())?, + )) + } +} + +impl Default for LoroList { + fn default() -> Self { + Self::new() } } @@ -471,7 +546,7 @@ impl LoroList { /// /// # Example /// ``` -/// # use loro::{LoroDoc, ToJson, ExpandType, LoroValue}; +/// # use loro::{LoroDoc, ToJson, ExpandType, LoroText, LoroValue}; /// # use serde_json::json; /// let doc = LoroDoc::new(); /// let map = doc.get_map("map"); @@ -481,9 +556,7 @@ impl LoroList { /// map.insert("deleted", LoroValue::Null).unwrap(); /// map.delete("deleted").unwrap(); /// let text = map -/// .insert_container("text", loro_internal::ContainerType::Text).unwrap() -/// .into_text() -/// .unwrap(); +/// .insert_container("text", LoroText::new()).unwrap(); /// text.insert(0, "Hello world!").unwrap(); /// assert_eq!( /// doc.get_deep_value().to_json_value(), @@ -502,7 +575,50 @@ pub struct LoroMap { handler: InnerMapHandler, } +impl SealedTrait for LoroMap {} +impl ContainerTrait for LoroMap { + type Handler = InnerMapHandler; + + fn to_container(&self) -> Container { + Container::Map(self.clone()) + } + + fn to_handler(&self) -> Self::Handler { + self.handler.clone() + } + + fn from_handler(handler: Self::Handler) -> Self { + Self { handler } + } + + fn is_attached(&self) -> bool { + self.handler.is_attached() + } + + fn get_attached(&self) -> Option { + self.handler.get_attached().map(Self::from_handler) + } + + fn try_from_container(container: Container) -> Option { + container.into_map().ok() + } +} + impl LoroMap { + /// Create a new container that is detached from the document. + /// + /// The edits on a detached container will not be persisted. + /// To attach the container to the document, please insert it into an attached container. + pub fn new() -> Self { + Self { + handler: InnerMapHandler::new_detached(), + } + } + + pub fn is_attached(&self) -> bool { + self.handler.is_attached() + } + pub fn delete(&self, key: &str) -> LoroResult<()> { self.handler.delete(key) } @@ -543,17 +659,19 @@ impl LoroMap { /// # Example /// /// ``` - /// # use loro::{LoroDoc, ContainerType, ToJson}; + /// # use loro::{LoroDoc, LoroText, ContainerType, ToJson}; /// # use serde_json::json; /// let doc = LoroDoc::new(); /// let map = doc.get_map("m"); - /// let text = map.insert_container("t", ContainerType::Text).unwrap().into_text().unwrap(); + /// let text = map.insert_container("t", LoroText::new()).unwrap(); /// text.insert(0, "12"); /// text.insert(0, "0"); /// assert_eq!(doc.get_deep_value().to_json_value(), json!({"m": {"t": "012"}})); /// ``` - pub fn insert_container(&self, key: &str, c_type: ContainerType) -> LoroResult { - Ok(Container::from(self.handler.insert_container(key, c_type)?)) + pub fn insert_container(&self, key: &str, child: C) -> LoroResult { + Ok(C::from_handler( + self.handler.insert_container(key, child.to_handler())?, + )) } pub fn get_value(&self) -> LoroValue { @@ -563,6 +681,19 @@ impl LoroMap { pub fn get_deep_value(&self) -> LoroValue { self.handler.get_deep_value() } + + pub fn get_or_create_container(&self, key: &str, child: C) -> LoroResult { + Ok(C::from_handler( + self.handler + .get_or_create_container(key, child.to_handler())?, + )) + } +} + +impl Default for LoroMap { + fn default() -> Self { + Self::new() + } } /// LoroText container. It's used to model plaintext/richtext. @@ -571,7 +702,54 @@ pub struct LoroText { handler: InnerTextHandler, } +impl SealedTrait for LoroText {} +impl ContainerTrait for LoroText { + type Handler = InnerTextHandler; + + fn to_container(&self) -> Container { + Container::Text(self.clone()) + } + + fn to_handler(&self) -> Self::Handler { + self.handler.clone() + } + + fn from_handler(handler: Self::Handler) -> Self { + Self { handler } + } + + fn is_attached(&self) -> bool { + self.handler.is_attached() + } + + fn get_attached(&self) -> Option { + self.handler.get_attached().map(Self::from_handler) + } + + fn try_from_container(container: Container) -> Option { + container.into_text().ok() + } +} + impl LoroText { + /// Create a new container that is detached from the document. + /// + /// The edits on a detached container will not be persisted. + /// To attach the container to the document, please insert it into an attached container. + pub fn new() -> Self { + Self { + handler: InnerTextHandler::new_detached(), + } + } + + /// Whether the container is attached to a document + /// + /// The edits on a detached container will not be persisted. + /// To attach the container to the document, please insert it into an attached container. + pub fn is_attached(&self) -> bool { + self.handler.is_attached() + } + /// Get the [ContainerID] of the text container. pub fn id(&self) -> ContainerID { self.handler.id().clone() @@ -689,6 +867,12 @@ impl LoroText { } } +impl Default for LoroText { + fn default() -> Self { + Self::new() + } +} + /// LoroTree container. It's used to model movable trees. /// /// You may use it to model directories, outline or other movable hierarchical data. @@ -697,7 +881,54 @@ pub struct LoroTree { handler: InnerTreeHandler, } +impl SealedTrait for LoroTree {} +impl ContainerTrait for LoroTree { + type Handler = InnerTreeHandler; + + fn to_container(&self) -> Container { + Container::Tree(self.clone()) + } + + fn to_handler(&self) -> Self::Handler { + self.handler.clone() + } + + fn from_handler(handler: Self::Handler) -> Self { + Self { handler } + } + + fn is_attached(&self) -> bool { + self.handler.is_attached() + } + + fn get_attached(&self) -> Option { + self.handler.get_attached().map(Self::from_handler) + } + + fn try_from_container(container: Container) -> Option { + container.into_tree().ok() + } +} + impl LoroTree { + /// Create a new container that is detached from the document. + /// + /// The edits on a detached container will not be persisted. + /// To attach the container to the document, please insert it into an attached container. + pub fn new() -> Self { + Self { + handler: InnerTreeHandler::new_detached(), + } + } + + /// Whether the container is attached to a document + /// + /// The edits on a detached container will not be persisted. + /// To attach the container to the document, please insert it into an attached container. + pub fn is_attached(&self) -> bool { + self.handler.is_attached() + } + /// Create a new tree node and return the [`TreeID`]. /// /// If the `parent` is `None`, the created node is the root of a tree. @@ -818,6 +1049,12 @@ impl LoroTree { } } +impl Default for LoroTree { + fn default() -> Self { + Self::new() + } +} + use enum_as_inner::EnumAsInner; /// All the CRDT containers supported by loro. @@ -829,7 +1066,73 @@ pub enum Container { Tree(LoroTree), } +impl SealedTrait for Container {} +impl ContainerTrait for Container { + type Handler = loro_internal::handler::Handler; + + fn to_container(&self) -> Container { + self.clone() + } + + fn to_handler(&self) -> Self::Handler { + match self { + Container::List(x) => Self::Handler::List(x.to_handler()), + Container::Map(x) => Self::Handler::Map(x.to_handler()), + Container::Text(x) => Self::Handler::Text(x.to_handler()), + Container::Tree(x) => Self::Handler::Tree(x.to_handler()), + } + } + + fn from_handler(handler: Self::Handler) -> Self { + match handler { + InnerHandler::Text(x) => Container::Text(LoroText { handler: x }), + InnerHandler::Map(x) => Container::Map(LoroMap { handler: x }), + InnerHandler::List(x) => Container::List(LoroList { handler: x }), + InnerHandler::Tree(x) => Container::Tree(LoroTree { handler: x }), + } + } + + fn is_attached(&self) -> bool { + match self { + Container::List(x) => x.is_attached(), + Container::Map(x) => x.is_attached(), + Container::Text(x) => x.is_attached(), + Container::Tree(x) => x.is_attached(), + } + } + + fn get_attached(&self) -> Option { + match self { + Container::List(x) => x.get_attached().map(Container::List), + Container::Map(x) => x.get_attached().map(Container::Map), + Container::Text(x) => x.get_attached().map(Container::Text), + Container::Tree(x) => x.get_attached().map(Container::Tree), + } + } + + fn try_from_container(container: Container) -> Option + where + Self: Sized, + { + Some(container) + } +} + impl Container { + /// Create a detached container of the given type. + /// + /// A detached container is a container that is not attached to a document. + /// The edits on a detached container will not be persisted. + /// To attach the container to the document, please insert it into an attached container. + pub fn new(kind: ContainerType) -> Self { + match kind { + ContainerType::List => Container::List(LoroList::new()), + ContainerType::Map => Container::Map(LoroMap::new()), + ContainerType::Text => Container::Text(LoroText::new()), + ContainerType::Tree => Container::Tree(LoroTree::new()), + } + } + pub fn get_type(&self) -> ContainerType { match self { Container::List(_) => ContainerType::List, diff --git a/crates/loro/tests/loro_rust_test.rs b/crates/loro/tests/loro_rust_test.rs index fe104e67..20441f7d 100644 --- a/crates/loro/tests/loro_rust_test.rs +++ b/crates/loro/tests/loro_rust_test.rs @@ -1,18 +1,16 @@ use std::{cmp::Ordering, sync::Arc}; -use loro::{FrontiersNotIncluded, LoroDoc, LoroError, ToJson}; +use loro::{FrontiersNotIncluded, LoroDoc, LoroError, LoroList, LoroMap, LoroText, ToJson}; use loro_internal::{handler::TextDelta, id::ID, LoroResult}; use serde_json::json; #[test] fn list_checkout() -> Result<(), LoroError> { let doc = LoroDoc::new(); - doc.get_list("list") - .insert_container(0, loro::ContainerType::Map)?; + doc.get_list("list").insert_container(0, LoroMap::new())?; doc.commit(); let f0 = doc.state_frontiers(); - doc.get_list("list") - .insert_container(0, loro::ContainerType::Text)?; + doc.get_list("list").insert_container(0, LoroText::new())?; doc.commit(); let f1 = doc.state_frontiers(); doc.get_list("list").delete(1, 1)?; @@ -263,10 +261,7 @@ fn map() -> LoroResult<()> { map.insert("null", LoroValue::Null)?; map.insert("deleted", LoroValue::Null)?; map.delete("deleted")?; - let text = map - .insert_container("text", loro_internal::ContainerType::Text)? - .into_text() - .unwrap(); + let text = map.insert_container("text", LoroText::new())?; text.insert(0, "Hello world!")?; assert_eq!( doc.get_deep_value().to_json_value(), @@ -397,3 +392,63 @@ fn subscribe() { doc.commit(); assert!(ran.load(std::sync::atomic::Ordering::Relaxed)); } + +#[test] +fn prelim_support() -> LoroResult<()> { + let map = LoroMap::new(); + map.insert("key", "value")?; + let text = LoroText::new(); + text.insert(0, "123")?; + let text = map.insert_container("text", text)?; + let doc = LoroDoc::new(); + let root_map = doc.get_map("map"); + let map = root_map.insert_container("child_map", map)?; + // `map` is now attached to the doc + map.insert("1", "223")?; // "223" now presents in the json value of doc + let list = map.insert_container("list", LoroList::new())?; // creating subcontainer will be easier + assert_eq!( + doc.get_deep_value().to_json_value(), + json!({ + "map": { + "child_map": { + "key": "value", + "1": "223", + "text": "123", + "list": [] + } + } + }) + ); + assert!(!text.is_attached()); + assert!(list.is_attached()); + text.insert(0, "56")?; + list.insert(0, 123)?; + assert_eq!( + doc.get_deep_value().to_json_value(), + json!({ + "map": { + "child_map": { + "key": "value", + "1": "223", + "text": "123", + "list": [123] + } + } + }) + ); + Ok(()) +} + +#[test] +fn init_example() { + // create meta/users/0/new_user/{name: string, bio: Text} + let doc = LoroDoc::new(); + let meta = doc.get_map("meta"); + let user = meta + .get_or_create_container("users", LoroList::new()) + .unwrap() + .insert_container(0, LoroMap::new()) + .unwrap(); + user.insert("name", "new_user").unwrap(); + user.insert_container("bio", LoroText::new()).unwrap(); +} diff --git a/crates/loro/tests/readme.rs b/crates/loro/tests/readme.rs new file mode 100644 index 00000000..12165603 --- /dev/null +++ b/crates/loro/tests/readme.rs @@ -0,0 +1,47 @@ +#[test] +fn readme_basic() { + use loro::ContainerTrait; + use loro::{LoroDoc, LoroList, LoroText, LoroValue, ToJson}; + use serde_json::json; + + let doc = LoroDoc::new(); + let map = doc.get_map("map"); + map.insert("key", "value").unwrap(); + map.insert("true", true).unwrap(); + map.insert("null", LoroValue::Null).unwrap(); + map.insert("deleted", LoroValue::Null).unwrap(); + map.delete("deleted").unwrap(); + let list = map.insert_container("list", LoroList::new()).unwrap(); + list.insert(0, "List").unwrap(); + list.insert(1, 9).unwrap(); + let old_text = LoroText::new(); + old_text.insert(0, "Hello ").unwrap(); + let text = map.insert_container("text", old_text.clone()).unwrap(); + text.insert(6, "world!").unwrap(); + assert_eq!( + doc.get_deep_value().to_json_value(), + json!({ + "map": { + "key": "value", + "true": true, + "null": null, + "list": ["List", 9], + "text": "Hello world!" + } + }) + ); + let new_text = old_text.get_attached().unwrap(); + new_text.insert(0, "New ").unwrap(); + assert_eq!( + doc.get_deep_value().to_json_value(), + json!({ + "map": { + "key": "value", + "true": true, + "null": null, + "list": ["List", 9], + "text": "New Hello world!" + } + }) + ); +} diff --git a/loro-js/src/index.ts b/loro-js/src/index.ts index 01a875ad..5620d18d 100644 --- a/loro-js/src/index.ts +++ b/loro-js/src/index.ts @@ -1,17 +1,17 @@ export * from "loro-wasm"; import { Container, + ContainerID, Delta, + Loro, + LoroList, + LoroMap, LoroText, LoroTree, LoroTreeNode, OpId, - Value, - ContainerID, - Loro, - LoroList, - LoroMap, TreeID, + Value, } from "loro-wasm"; Loro.prototype.getTypedMap = function (...args) { @@ -179,14 +179,10 @@ export function isContainer(value: any): value is Container { */ export function getType( value: T, -): T extends LoroText - ? "Text" - : T extends LoroMap - ? "Map" - : T extends LoroTree - ? "Tree" - : T extends LoroList - ? "List" +): T extends LoroText ? "Text" + : T extends LoroMap ? "Map" + : T extends LoroTree ? "Tree" + : T extends LoroList ? "List" : "Json" { if (isContainer(value)) { return value.kind(); @@ -210,15 +206,12 @@ declare module "loro-wasm" { } interface LoroList { - insertContainer(pos: number, container: "Map"): LoroMap; - insertContainer(pos: number, container: "List"): LoroList; - insertContainer(pos: number, container: "Text"): LoroText; - insertContainer(pos: number, container: "Tree"): LoroTree; - insertContainer(pos: number, container: string): never; + insertContainer(pos: number, child: C): C; get(index: number): undefined | Value | Container; getTyped(loro: Loro, index: Key): T[Key]; insertTyped(pos: Key, value: T[Key]): void; + insert(pos: number, value: Container): never; insert(pos: number, value: Value): void; delete(pos: number, len: number): void; subscribe(txn: Loro, listener: Listener): number; @@ -231,11 +224,7 @@ declare module "loro-wasm" { getOrCreateContainer(key: string, container_type: "Tree"): LoroTree; getOrCreateContainer(key: string, container_type: string): never; - setContainer(key: string, container_type: "Map"): LoroMap; - setContainer(key: string, container_type: "List"): LoroList; - setContainer(key: string, container_type: "Text"): LoroText; - setContainer(key: string, container_type: "Tree"): LoroTree; - setContainer(key: string, container_type: string): never; + setContainer(key: string, child: C): C; get(key: string): undefined | Value | Container; getTyped(txn: Loro, key: Key): T[Key]; diff --git a/loro-js/tests/basic.test.ts b/loro-js/tests/basic.test.ts index 1754e8c8..7301cfa9 100644 --- a/loro-js/tests/basic.test.ts +++ b/loro-js/tests/basic.test.ts @@ -1,13 +1,14 @@ import { describe, expect, expectTypeOf, it } from "vitest"; import { + Container, getType, isContainer, Loro, LoroList, LoroMap, - VersionVector, + LoroText, + LoroTree, } from "../src"; -import { Container } from "../dist/loro"; it("basic example", () => { const doc = new Loro(); @@ -32,7 +33,7 @@ it("basic example", () => { }); // Insert a text container to the list - const text = list.insertContainer(0, "Text"); + const text = list.insertContainer(0, new LoroText()); text.insert(0, "Hello"); text.insert(0, "Hi! "); @@ -43,7 +44,7 @@ it("basic example", () => { }); // Insert a list container to the map - const list2 = map.setContainer("test", "List"); + const list2 = map.setContainer("test", new LoroList()); list2.insert(0, 1); expect(doc.toJson()).toStrictEqual({ list: ["Hi! Hello", "C"], @@ -54,10 +55,10 @@ it("basic example", () => { it("get or create on Map", () => { const docA = new Loro(); const map = docA.getMap("map"); - const container = map.getOrCreateContainer("list", "List"); + const container = map.getOrCreateContainer("list", new LoroList()); container.insert(0, 1); container.insert(0, 2); - const text = map.getOrCreateContainer("text", "Text"); + const text = map.getOrCreateContainer("text", new LoroText()); text.insert(0, "Hello"); expect(docA.toJson()).toStrictEqual({ map: { list: [2, 1], text: "Hello" }, @@ -99,7 +100,7 @@ describe("list", () => { it("insert containers", () => { const doc = new Loro(); const list = doc.getList("list"); - const map = list.insertContainer(0, "Map"); + const map = list.insertContainer(0, new LoroMap()); map.set("key", "value"); const v = list.get(0) as LoroMap; console.log(v); @@ -113,7 +114,7 @@ describe("list", () => { list.insert(0, 1); list.insert(1, 2); expect(list.toArray()).toStrictEqual([1, 2]); - list.insertContainer(2, "Text"); + list.insertContainer(2, new LoroText()); const t = list.toArray()[2]; expect(isContainer(t)).toBeTruthy(); expect(getType(t)).toBe("Text"); @@ -125,7 +126,7 @@ describe("map", () => { it("get child container", () => { const doc = new Loro(); const map = doc.getMap("map"); - const list = map.setContainer("key", "List"); + const list = map.setContainer("key", new LoroList()); list.insert(0, 1); expect(map.get("key") instanceof LoroList).toBeTruthy(); expect((map.get("key") as LoroList).toJson()).toStrictEqual([1]); @@ -228,7 +229,7 @@ describe("map", () => { it("entries should return container handlers", () => { const doc = new Loro(); const map = doc.getMap("map"); - map.setContainer("text", "Text"); + map.setContainer("text", new LoroText()); map.set("foo", "bar"); const entries = map.entries(); expect((entries[0][1]! as Container).kind() === "Text").toBeTruthy(); @@ -393,13 +394,52 @@ it("get container parent", () => { const doc = new Loro(); const m = doc.getMap("m"); expect(m.parent()).toBeUndefined(); - const list = m.setContainer("t", "List"); + const list = m.setContainer("t", new LoroList()); expect(list.parent()!.id).toBe(m.id); - const text = list.insertContainer(0, "Text"); + const text = list.insertContainer(0, new LoroText()); expect(text.parent()!.id).toBe(list.id); - const tree = list.insertContainer(1, "Tree"); + const tree = list.insertContainer(1, new LoroTree()); expect(tree.parent()!.id).toBe(list.id); const treeNode = tree.createNode(); - const subtext = treeNode.data.setContainer("t", "Text"); + const subtext = treeNode.data.setContainer("t", new LoroText()); expect(subtext.parent()!.id).toBe(treeNode.data.id); }); + +it("prelim support", () => { + // Now we can create a new container directly + const map = new LoroMap(); + map.set("3", 2); + const list = new LoroList(); + list.insertContainer(0, map); + // map should still be valid + map.set("9", 9); + // the type of setContainer/insertContainer changed + const text = map.setContainer("text", new LoroText()); + { + // Changes will be reflected in the container tree + text.insert(0, "Heello"); + expect(list.toJson()).toStrictEqual([{ "3": 2, "9": 9, text: "Heello" }]); + text.delete(1, 1); + expect(list.toJson()).toStrictEqual([{ "3": 2, "9": 9, text: "Hello" }]); + } + const doc = new Loro(); + const rootMap = doc.getMap("map"); + rootMap.setContainer("test", map); // new way to create sub-container + + // Use getAttached() to get the attached version of text + const attachedText = text.getAttached()!; + expect(text.isAttached()).toBeFalsy(); + expect(attachedText.isAttached()).toBeTruthy(); + text.insert(0, "Detached "); + attachedText.insert(0, "Attached "); + expect(text.toString()).toBe("Detached Hello"); + expect(doc.toJson()).toStrictEqual({ + map: { + test: { + "3": 2, + "9": 9, + text: "Attached Hello", + }, + }, + }); +}); diff --git a/loro-js/tests/event.test.ts b/loro-js/tests/event.test.ts index 84fea78b..bc706019 100644 --- a/loro-js/tests/event.test.ts +++ b/loro-js/tests/event.test.ts @@ -5,6 +5,8 @@ import { ListDiff, Loro, LoroEventBatch, + LoroList, + LoroMap, LoroText, MapDiff, TextDiff, @@ -32,14 +34,14 @@ describe("event", () => { lastEvent = event; }); const map = loro.getMap("map"); - const subMap = map.setContainer("sub", "Map"); + const subMap = map.setContainer("sub", new LoroMap()); subMap.set("0", "1"); loro.commit(); await oneMs(); expect(lastEvent?.events[1].path).toStrictEqual(["map", "sub"]); - const list = subMap.setContainer("list", "List"); + const list = subMap.setContainer("list", new LoroList()); list.insert(0, "2"); - const text = list.insertContainer(1, "Text"); + const text = list.insertContainer(1, new LoroText()); loro.commit(); await oneMs(); text.insert(0, "3"); @@ -184,11 +186,11 @@ describe("event", () => { times += 1; }); - const subMap = map.setContainer("sub", "Map"); + const subMap = map.setContainer("sub", new LoroMap()); loro.commit(); await oneMs(); expect(times).toBe(1); - const text = subMap.setContainer("k", "Text"); + const text = subMap.setContainer("k", new LoroText()); loro.commit(); await oneMs(); expect(times).toBe(2); @@ -213,7 +215,7 @@ describe("event", () => { times += 1; }); - const text = list.insertContainer(0, "Text"); + const text = list.insertContainer(0, new LoroText()); loro.commit(); await oneMs(); expect(times).toBe(1); @@ -294,7 +296,7 @@ describe("event", () => { first = false; } }); - list.insertContainer(0, "Text"); + list.insertContainer(0, new LoroText()); loro.commit(); await oneMs(); expect(loro.toJson().list[0]).toBe("abc"); @@ -318,8 +320,8 @@ describe("event", () => { } }); - list.insertContainer(0, "Map"); - const t = list.insertContainer(0, "Text"); + list.insertContainer(0, new LoroMap()); + const t = list.insertContainer(0, new LoroText()); t.insert(0, "He"); t.insert(2, "llo"); doc.commit(); diff --git a/loro-js/tests/misc.test.ts b/loro-js/tests/misc.test.ts index ca5ecc90..d8abdccd 100644 --- a/loro-js/tests/misc.test.ts +++ b/loro-js/tests/misc.test.ts @@ -1,10 +1,5 @@ import { describe, expect, it } from "vitest"; -import { - Loro, - LoroList, - LoroMap, - VersionVector, -} from "../src"; +import { Loro, LoroList, LoroMap, LoroText, VersionVector } from "../src"; import { expectTypeOf } from "vitest"; function assertEquals(a: any, b: any) { @@ -188,7 +183,7 @@ describe("wasm", () => { b.set("ab", 123); loro.commit(); - const bText = b.setContainer("hh", "Text"); + const bText = b.setContainer("hh", new LoroText()); loro.commit(); it("map get", () => { @@ -222,7 +217,7 @@ describe("type", () => { it("test recursive map type", () => { const loro = new Loro<{ map: LoroMap<{ map: LoroMap<{ name: "he" }> }> }>(); const map = loro.getTypedMap("map"); - map.setContainer("map", "Map"); + map.setContainer("map", new LoroMap()); const subMap = map.getTyped(loro, "map"); const name = subMap.getTyped(loro, "name"); @@ -260,7 +255,7 @@ describe("tree", () => { assertEquals(child.parent()!.id, root.id); }); - it("move",()=>{ + it("move", () => { const root = tree.createNode(); const child = root.createNode(); const child2 = root.createNode(); @@ -268,7 +263,7 @@ describe("tree", () => { child2.moveTo(child); assertEquals(child2.parent()!.id, child.id); assertEquals(child.children()[0].id, child2.id); - }) + }); it("meta", () => { const root = tree.createNode(); diff --git a/loro-js/tests/version.test.ts b/loro-js/tests/version.test.ts index 83eb30f3..6ac14c56 100644 --- a/loro-js/tests/version.test.ts +++ b/loro-js/tests/version.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { Loro, OpId, VersionVector } from "../src"; +import { Loro, LoroMap, OpId, VersionVector } from "../src"; describe("Frontiers", () => { it("two clients", () => { @@ -40,32 +40,50 @@ describe("Frontiers", () => { doc1.getText("text").insert(0, "01234"); doc1.commit(); - expect(() => { doc1.cmpFrontiers([{ peer: "1", counter: 1 }], [{ peer: "2", counter: 10 }]) }).toThrow(); - expect(doc1.cmpFrontiers([], [{ peer: "1", counter: 1 }])).toBe(-1) - expect(doc1.cmpFrontiers([], [])).toBe(0) - expect(doc1.cmpFrontiers([{ peer: "1", counter: 4 }], [{ peer: "2", counter: 3 }])).toBe(-1) - expect(doc1.cmpFrontiers([{ peer: "1", counter: 5 }], [{ peer: "2", counter: 3 }])).toBe(1) - }) + expect(() => { + doc1.cmpFrontiers([{ peer: "1", counter: 1 }], [{ + peer: "2", + counter: 10, + }]); + }).toThrow(); + expect(doc1.cmpFrontiers([], [{ peer: "1", counter: 1 }])).toBe(-1); + expect(doc1.cmpFrontiers([], [])).toBe(0); + expect( + doc1.cmpFrontiers([{ peer: "1", counter: 4 }], [{ + peer: "2", + counter: 3, + }]), + ).toBe(-1); + expect( + doc1.cmpFrontiers([{ peer: "1", counter: 5 }], [{ + peer: "2", + counter: 3, + }]), + ).toBe(1); + }); }); -it('peer id repr should be consistent', () => { +it("peer id repr should be consistent", () => { const doc = new Loro(); const id = doc.peerIdStr; doc.getText("text").insert(0, "hello"); doc.commit(); const f = doc.frontiers(); expect(f[0].peer).toBe(id); - const map = doc.getList("list").insertContainer(0, "Map"); + const child = new LoroMap(); + console.dir(child); + const map = doc.getList("list").insertContainer(0, child); + console.dir(child); const mapId = map.id; - const peerIdInContainerId = mapId.split(":")[1].split("@")[1] + const peerIdInContainerId = mapId.split(":")[1].split("@")[1]; expect(peerIdInContainerId).toBe(id); doc.commit(); expect(doc.version().get(id)).toBe(6); expect(doc.version().toJSON().get(id)).toBe(6); const m = doc.getMap(mapId); m.set("0", 1); - expect(map.get("0")).toBe(1) -}) + expect(map.get("0")).toBe(1); +}); describe("Version", () => { const a = new Loro(); @@ -83,13 +101,17 @@ describe("Version", () => { const vv = new Map(); vv.set("0", 3); vv.set("1", 2); - expect((a.version().toJSON())).toStrictEqual(vv); - expect((a.version().toJSON())).toStrictEqual(vv); - expect(a.vvToFrontiers(new VersionVector(vv))).toStrictEqual(a.frontiers()); + expect(a.version().toJSON()).toStrictEqual(vv); + expect(a.version().toJSON()).toStrictEqual(vv); + expect(a.vvToFrontiers(new VersionVector(vv))).toStrictEqual( + a.frontiers(), + ); const v = a.version(); const temp = a.vvToFrontiers(v); expect(temp).toStrictEqual(a.frontiers()); - expect(a.frontiers()).toStrictEqual([{ peer: "0", counter: 2 }] as OpId[]); + expect(a.frontiers()).toStrictEqual( + [{ peer: "0", counter: 2 }] as OpId[], + ); } }); diff --git a/loro-js/tsconfig.json b/loro-js/tsconfig.json index 696e3be2..8d9fac11 100644 --- a/loro-js/tsconfig.json +++ b/loro-js/tsconfig.json @@ -1,7 +1,6 @@ { "compilerOptions": { /* Visit https://aka.ms/tsconfig to read more about this file */ - /* Projects */ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ @@ -9,7 +8,6 @@ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - /* Language and Environment */ "target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ @@ -23,7 +21,6 @@ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - /* Modules */ "module": "commonjs" /* Specify what module code is generated. */, // "rootDir": "./", /* Specify the root folder within your source files. */ @@ -42,12 +39,10 @@ // "resolveJsonModule": true, /* Enable importing .json files. */ // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - /* JavaScript Support */ // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - /* Emit */ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */ @@ -55,7 +50,7 @@ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ + "outDir": "./dist", /* Specify an output folder for all emitted files. */ // "removeComments": true, /* Disable emitting comments. */ // "noEmit": true, /* Disable emitting files from a compilation. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ @@ -72,7 +67,6 @@ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - /* Interop Constraints */ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ @@ -80,7 +74,6 @@ "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - /* Type Checking */ "strict": true /* Enable all strict type-checking options. */, // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ @@ -101,7 +94,6 @@ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */