refactor!: Add prelim support, making creating sub container easier (#300)

This PR includes a BREAKING CHANGE.

It enables you to create containers before attaching them to the document, making the API more intuitive and straightforward.

A container can be either attached to a document or detached. When it's detached, its history/state does not persist. You can attach a container to a document by inserting it into an attached container. Once a container is attached, its state, along with all of its descendants's states, will be recreated in the document. After attaching, the container and its descendants, will each have their corresponding "attached" version of themselves?

When a detached container x is attached to a document, you can use x.getAttached() to obtain the corresponding attached container.
This commit is contained in:
Zixuan Chen 2024-03-30 11:38:24 +08:00 committed by GitHub
parent edb0ef75f6
commit 9ecc0a90b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 2326 additions and 733 deletions

View file

@ -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();

View file

@ -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 {

View file

@ -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) => {

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)]

View file

@ -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);
}

View file

@ -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(),

View file

@ -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;

View file

@ -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();

View file

@ -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<Actor> {
}
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<Actor> {
}
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);

File diff suppressed because it is too large Load diff

View file

@ -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<I: IntoContainerId>(&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<I: IntoContainerId>(&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<I: IntoContainerId>(&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<I: IntoContainerId>(&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<I: IntoContainerId>(&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()

View file

@ -166,12 +166,8 @@ impl AppDag {
pub fn frontiers_to_vv(&self, frontiers: &Frontiers) -> Option<VersionVector> {
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);
}

View file

@ -693,6 +693,7 @@ impl DocState {
}
#[inline(always)]
#[allow(unused)]
pub(crate) fn with_state<F, R>(&mut self, idx: ContainerIdx, f: F) -> R
where
F: FnOnce(&State) -> R,

View file

@ -379,7 +379,7 @@ impl Transaction {
/// if it's str it will use Root container, which will not be None
pub fn get_text<I: IntoContainerId>(&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<I: IntoContainerId>(&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<I: IntoContainerId>(&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<I: IntoContainerId>(&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<I: IntoContainerId>(&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)
}

View file

@ -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"]}))

View file

@ -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();

View file

@ -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<T: FromWasmAbi<Abi = u32>>(
js: JsValue,
struct_name: &str,
) -> Result<T, JsValue> {
pub(crate) fn js_to_container(js: JsContainer) -> Result<Container, JsValue> {
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<JsValue> for LoroText {
type Error = JsValue;
fn try_from(value: JsValue) -> Result<Self, Self::Error> {
js_to_any(value, "LoroText")
}
}
impl TryFrom<JsValue> for LoroList {
type Error = JsValue;
fn try_from(value: JsValue) -> Result<Self, Self::Error> {
js_to_any(value, "LoroList")
}
}
impl TryFrom<JsValue> for LoroMap {
type Error = JsValue;
fn try_from(value: JsValue) -> Result<Self, Self::Error> {
js_to_any(value, "LoroMap")
}
Ok(container)
}
pub(crate) fn resolved_diff_to_js(value: &Diff, doc: &Arc<LoroDoc>) -> JsValue {
@ -130,7 +127,7 @@ fn delta_item_to_js(item: DeltaItem<Vec<ValueOrHandler>, ()>, doc: &Arc<LoroDoc>
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<LoroDoc>) -> 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<LoroDoc>) -> JsValue {
obj.into_js_result().unwrap()
}
pub(crate) fn handler_to_js_value(handler: Handler, doc: Arc<LoroDoc>) -> JsValue {
pub(crate) fn handler_to_js_value(handler: Handler, doc: Option<Arc<LoroDoc>>) -> JsValue {
match handler {
Handler::Text(t) => LoroText { handler: t, doc }.into(),
Handler::Map(m) => LoroMap { handler: m, doc }.into(),

View file

@ -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<PeerID, number> | 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<LoroDoc>,
doc: Option<Arc<LoroDoc>>,
}
#[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<LoroDoc>,
doc: Option<Arc<LoroDoc>>,
}
#[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<JsContainerOrUndefined> {
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<JsContainer> {
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<JsValue> {
let mut ans: Vec<JsValue> = 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<JsValue> {
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<JsContainer> {
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<LoroDoc>,
doc: Option<Arc<LoroDoc>>,
}
#[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<JsValue> {
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<JsContainer> {
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<LoroDoc>,
doc: Option<Arc<LoroDoc>>,
}
#[wasm_bindgen]
pub struct LoroTreeNode {
id: TreeID,
tree: TreeHandler,
doc: Arc<LoroDoc>,
doc: Option<Arc<LoroDoc>>,
}
#[wasm_bindgen]
impl LoroTreeNode {
fn from_tree(id: TreeID, tree: TreeHandler, doc: Arc<LoroDoc>) -> Self {
fn from_tree(id: TreeID, tree: TreeHandler, doc: Option<Arc<LoroDoc>>) -> 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<LoroDoc>) -> JsValue {
impl Default for LoroTree {
fn default() -> Self {
Self::new()
}
}
fn loro_value_to_js_value_or_container(
value: ValueOrHandler,
doc: Option<Arc<LoroDoc>>,
) -> JsValue {
match value {
ValueOrHandler::Value(v) => {
let value: JsValue = v.into();
@ -2339,6 +2505,24 @@ fn id_value_to_u64(value: JsValue) -> JsResult<u64> {
}
}
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#"
/**

View file

@ -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(),

View file

@ -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<Self>
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<Self>
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> {
self.handler.get_attached().map(Self::from_handler)
}
fn try_from_container(container: Container) -> Option<Self> {
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<LoroValue>) -> LoroResult<()> {
self.handler.insert(pos, v)
}
@ -423,9 +488,11 @@ impl LoroList {
}
#[inline]
pub fn push_container(&self, c_type: ContainerType) -> LoroResult<Container> {
pub fn push_container<C: ContainerTrait>(&self, child: C) -> LoroResult<C> {
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<I>(&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<Container> {
Ok(Container::from(self.handler.insert_container(pos, c_type)?))
pub fn insert_container<C: ContainerTrait>(&self, pos: usize, child: C) -> LoroResult<C> {
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> {
self.handler.get_attached().map(Self::from_handler)
}
fn try_from_container(container: Container) -> Option<Self> {
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<Container> {
Ok(Container::from(self.handler.insert_container(key, c_type)?))
pub fn insert_container<C: ContainerTrait>(&self, key: &str, child: C) -> LoroResult<C> {
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<C: ContainerTrait>(&self, key: &str, child: C) -> LoroResult<C> {
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> {
self.handler.get_attached().map(Self::from_handler)
}
fn try_from_container(container: Container) -> Option<Self> {
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> {
self.handler.get_attached().map(Self::from_handler)
}
fn try_from_container(container: Container) -> Option<Self> {
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<Self> {
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<Self>
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,

View file

@ -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();
}

View file

@ -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!"
}
})
);
}

View file

@ -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<T>(
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<T extends any[] = any[]> {
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<C extends Container>(pos: number, child: C): C;
get(index: number): undefined | Value | Container;
getTyped<Key extends keyof T & number>(loro: Loro, index: Key): T[Key];
insertTyped<Key extends keyof T & number>(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<C extends Container>(key: string, child: C): C;
get(key: string): undefined | Value | Container;
getTyped<Key extends keyof T & string>(txn: Loro, key: Key): T[Key];

View file

@ -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",
},
},
});
});

View file

@ -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();

View file

@ -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();

View file

@ -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[],
);
}
});

View file

@ -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 '<reference>'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. */