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. */