diff --git a/.vscode/extensions.json b/.vscode/extensions.json index a8409eb2..27e167f9 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,6 +2,7 @@ "recommendations": [ "skellock.just", "Gruntfuggly.todo-tree", - "rust-lang.rust-analyzer" + "rust-lang.rust-analyzer", + "denoland.vscode-deno" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 88343209..1bc59811 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -19,5 +19,6 @@ "explorer.fileNesting.patterns": { "*.rs": "${capture}.excalidraw" }, - "excalidraw.theme": "dark" + "excalidraw.theme": "dark", + "deno.enable": true } diff --git a/crates/loro-core/src/container/manager.rs b/crates/loro-core/src/container/manager.rs index e08467c3..0e6b0376 100644 --- a/crates/loro-core/src/container/manager.rs +++ b/crates/loro-core/src/container/manager.rs @@ -1,14 +1,18 @@ use std::{ ops::{Deref, DerefMut}, ptr::NonNull, - sync::RwLockWriteGuard, }; use enum_as_inner::EnumAsInner; use fxhash::FxHashMap; -use owning_ref::OwningRefMut; +use owning_ref::{OwningRef, OwningRefMut}; -use crate::{isomorph::IsoRefMut, log_store::LogStoreWeakRef, span::IdSpan, LogStore}; +use crate::{ + isomorph::{IsoRef, IsoRefMut}, + log_store::LogStoreWeakRef, + span::IdSpan, + LogStore, LoroError, +}; use super::{ map::MapContainer, text::text_container::TextContainer, Container, ContainerID, ContainerType, @@ -96,7 +100,9 @@ impl ContainerManager { log_store: LogStoreWeakRef, ) -> ContainerInstance { match container_type { - ContainerType::Map => ContainerInstance::Map(Box::new(MapContainer::new(id))), + ContainerType::Map => { + ContainerInstance::Map(Box::new(MapContainer::new(id, log_store))) + } ContainerType::Text => { ContainerInstance::Text(Box::new(TextContainer::new(id, log_store))) } @@ -123,27 +129,46 @@ impl ContainerManager { &mut self, id: &ContainerID, log_store: LogStoreWeakRef, - ) -> &mut ContainerInstance { + ) -> Result<&mut ContainerInstance, LoroError> { if !self.containers.contains_key(id) { let container = self.create(id.clone(), id.container_type(), log_store); self.insert(id.clone(), container); } - self.get_mut(id).unwrap() + let container = self.get_mut(id).unwrap(); + if container.type_() != id.container_type() { + Err(LoroError::ContainerTypeError { + id: id.clone(), + actual_type: container.type_(), + expected_type: id.container_type(), + }) + } else { + Ok(container) + } } } -pub struct ContainerRef<'a, T> { +pub struct ContainerRefMut<'a, T> { value: OwningRefMut, Box>, } -impl<'a, T> From, Box>> for ContainerRef<'a, T> { +pub struct ContainerRef<'a, T> { + value: OwningRef, Box>, +} + +impl<'a, T> From, Box>> for ContainerRefMut<'a, T> { fn from(value: OwningRefMut, Box>) -> Self { + ContainerRefMut { value } + } +} + +impl<'a, T> From, Box>> for ContainerRef<'a, T> { + fn from(value: OwningRef, Box>) -> Self { ContainerRef { value } } } -impl<'a, T> Deref for ContainerRef<'a, T> { +impl<'a, T> Deref for ContainerRefMut<'a, T> { type Target = T; fn deref(&self) -> &Self::Target { @@ -151,7 +176,7 @@ impl<'a, T> Deref for ContainerRef<'a, T> { } } -impl<'a, T> DerefMut for ContainerRef<'a, T> { +impl<'a, T> DerefMut for ContainerRefMut<'a, T> { fn deref_mut(&mut self) -> &mut Self::Target { self.value.deref_mut() } diff --git a/crates/loro-core/src/container/map/map_container.rs b/crates/loro-core/src/container/map/map_container.rs index 3b5faca8..fa8e8692 100644 --- a/crates/loro-core/src/container/map/map_container.rs +++ b/crates/loro-core/src/container/map/map_container.rs @@ -22,6 +22,7 @@ pub struct MapContainer { id: ContainerID, state: FxHashMap, value: Option, + store: LogStoreWeakRef, } #[derive(Debug)] @@ -33,23 +34,18 @@ struct ValueSlot { impl MapContainer { #[inline] - pub fn new(id: ContainerID) -> Self { + pub(crate) fn new(id: ContainerID, store: LogStoreWeakRef) -> Self { MapContainer { id, + store, state: FxHashMap::default(), value: None, } } - // FIXME: keep store in the struct - pub(crate) fn insert( - &mut self, - key: InternalString, - value: InsertValue, - store: LogStoreWeakRef, - ) { + pub fn insert(&mut self, key: InternalString, value: InsertValue) { let self_id = self.id.clone(); - let m = store.upgrade().unwrap(); + let m = self.store.upgrade().unwrap(); let mut store = m.write(); let client_id = store.this_client_id; let order = TotalOrderStamp { @@ -89,10 +85,9 @@ impl MapContainer { ); } - // FIXME: keep store in the struct #[inline] - pub(crate) fn delete(&mut self, key: InternalString, store: LogStoreWeakRef) { - self.insert(key, InsertValue::Null, store); + pub fn delete(&mut self, key: InternalString) { + self.insert(key, InsertValue::Null); } } diff --git a/crates/loro-core/src/error.rs b/crates/loro-core/src/error.rs index 58eaacd9..19cec0a1 100644 --- a/crates/loro-core/src/error.rs +++ b/crates/loro-core/src/error.rs @@ -1,7 +1,15 @@ use thiserror::Error; +use crate::{container::ContainerID, ContainerType}; + #[derive(Error, Debug)] pub enum LoroError { + #[error("Expect container with the id of {id:?} has type {expected_type:?} but the actual type is {actual_type:?}.")] + ContainerTypeError { + id: ContainerID, + actual_type: ContainerType, + expected_type: ContainerType, + }, // #[error("the data for key `{0}` is not available")] // Redaction(String), // #[error("invalid header (expected {expected:?}, found {found:?})")] @@ -9,3 +17,16 @@ pub enum LoroError { // #[error("unknown data store error")] // Unknown, } + +#[cfg(feature = "wasm")] +pub mod wasm { + use wasm_bindgen::JsValue; + + use crate::LoroError; + + impl From for JsValue { + fn from(value: LoroError) -> Self { + JsValue::from_str(&value.to_string()) + } + } +} diff --git a/crates/loro-core/src/fuzz.rs b/crates/loro-core/src/fuzz.rs index 254425a9..ae9560ac 100644 --- a/crates/loro-core/src/fuzz.rs +++ b/crates/loro-core/src/fuzz.rs @@ -177,12 +177,14 @@ impl Actionable for Vec { match action { Action::Ins { content, pos, site } => { self[*site as usize] - .get_or_create_text_container_mut("text".into()) + .get_or_create_text_container("text".into()) + .unwrap() .insert(*pos, content); } Action::Del { pos, len, site } => { self[*site as usize] - .get_or_create_text_container_mut("text".into()) + .get_or_create_text_container("text".into()) + .unwrap() .delete(*pos, *len); } Action::Sync { from, to } => { @@ -198,7 +200,9 @@ impl Actionable for Vec { match action { Action::Ins { pos, site, .. } => { *site %= self.len() as u8; - let mut text = self[*site as usize].get_or_create_text_container_mut("text".into()); + let mut text = self[*site as usize] + .get_or_create_text_container("text".into()) + .unwrap(); let value = text.get_value().as_string().unwrap(); *pos %= value.len() + 1; while !value.is_char_boundary(*pos) { @@ -207,7 +211,9 @@ impl Actionable for Vec { } Action::Del { pos, len, site } => { *site %= self.len() as u8; - let mut text = self[*site as usize].get_or_create_text_container_mut("text".into()); + let mut text = self[*site as usize] + .get_or_create_text_container("text".into()) + .unwrap(); if text.text_len() == 0 { *len = 0; *pos = 0; @@ -235,8 +241,8 @@ impl Actionable for Vec { } fn check_eq(site_a: &mut LoroCore, site_b: &mut LoroCore) { - let mut a = site_a.get_or_create_text_container_mut("text".into()); - let mut b = site_b.get_or_create_text_container_mut("text".into()); + let mut a = site_a.get_or_create_text_container("text".into()).unwrap(); + let mut b = site_b.get_or_create_text_container("text".into()).unwrap(); let value_a = a.get_value(); let value_b = b.get_value(); assert_eq!(value_a.as_string().unwrap(), value_b.as_string().unwrap()); @@ -259,7 +265,7 @@ fn check_synced(sites: &mut [LoroCore]) { pub fn test_single_client(mut actions: Vec) { let mut store = LoroCore::new(Default::default(), Some(1)); - let mut text_container = store.get_or_create_text_container_mut("haha".into()); + let mut text_container = store.get_or_create_text_container("haha".into()).unwrap(); let mut ground_truth = String::new(); let mut applied = Vec::new(); for action in actions diff --git a/crates/loro-core/src/lib.rs b/crates/loro-core/src/lib.rs index 1daa88d3..a8398b61 100644 --- a/crates/loro-core/src/lib.rs +++ b/crates/loro-core/src/lib.rs @@ -28,6 +28,7 @@ pub mod tests; mod value; +pub use error::LoroError; pub(crate) mod macros; pub(crate) use change::{Lamport, Timestamp}; pub(crate) use id::{ClientID, ID}; @@ -35,10 +36,10 @@ pub(crate) use op::{ContentType, InsertContentTrait, Op, OpType}; pub(crate) type InternalString = DefaultAtom; -pub use container::ContainerType; +pub use container::{Container, ContainerType}; pub use log_store::LogStore; pub use loro::*; -pub use value::LoroValue; +pub use value::{InsertValue, LoroValue}; pub use version::VersionVector; use string_cache::DefaultAtom; diff --git a/crates/loro-core/src/log_store.rs b/crates/loro-core/src/log_store.rs index 393aeefc..91fda4c9 100644 --- a/crates/loro-core/src/log_store.rs +++ b/crates/loro-core/src/log_store.rs @@ -2,9 +2,7 @@ //! //! mod iter; -use std::{ - marker::PhantomPinned, -}; +use std::marker::PhantomPinned; use fxhash::{FxHashMap, FxHashSet}; @@ -149,7 +147,9 @@ impl LogStore { change: &mut Change, ) { for op in change.ops.vec_mut().iter_mut() { - let container = container_manager.get_or_create(&op.container, self.to_self.clone()); + let container = container_manager + .get_or_create(&op.container, self.to_self.clone()) + .unwrap(); container.to_import(op); } } @@ -282,7 +282,9 @@ impl LogStore { } for container in set { - let container = container_manager.get_or_create(container, self.to_self.clone()); + let container = container_manager + .get_or_create(container, self.to_self.clone()) + .unwrap(); container.apply(change.id_span(), self); } diff --git a/crates/loro-core/src/loro.rs b/crates/loro-core/src/loro.rs index 3c66ec6f..0b5137c3 100644 --- a/crates/loro-core/src/loro.rs +++ b/crates/loro-core/src/loro.rs @@ -6,14 +6,14 @@ use crate::{ change::Change, configure::Configure, container::{ - manager::{ContainerInstance, ContainerManager, ContainerRef}, + manager::{ContainerManager, ContainerRef, ContainerRefMut}, map::MapContainer, text::text_container::TextContainer, ContainerID, ContainerType, }, id::ClientID, isomorph::{Irc, IsoRefMut, IsoRw}, - LogStore, VersionVector, + LogStore, LoroError, VersionVector, }; pub struct LoroCore { @@ -44,59 +44,67 @@ impl LoroCore { self.log_store.read().get_vv().clone() } - pub fn get_container( + #[inline(always)] + pub fn get_or_create_map_container( &mut self, name: &str, - container: ContainerType, - ) -> OwningRefMut, ContainerInstance> { - let a = OwningRefMut::new(self.container.write()); - a.map_mut(|x| { - x.get_or_create( - &ContainerID::new_root(name, container), - Irc::downgrade(&self.log_store), - ) - }) + ) -> Result, LoroError> { + let mut a = OwningRefMut::new(self.container.write()); + let id = ContainerID::new_root(name, ContainerType::Map); + let ptr = Irc::downgrade(&self.log_store); + a.get_or_create(&id, ptr)?; + Ok( + a.map_mut(move |x| x.get_mut(&id).unwrap().as_map_mut().unwrap()) + .into(), + ) } - pub fn get_map_container( + #[inline(always)] + pub fn get_or_create_text_container( &mut self, name: &str, - ) -> OwningRefMut, Box> { - let a = OwningRefMut::new(self.container.write()); - a.map_mut(|x| { - x.get_or_create( - &ContainerID::new_root(name, ContainerType::Map), - Irc::downgrade(&self.log_store), - ) - .as_map_mut() - .unwrap() - }) + ) -> Result, LoroError> { + let mut a = OwningRefMut::new(self.container.write()); + let id = ContainerID::new_root(name, ContainerType::Text); + let ptr = Irc::downgrade(&self.log_store); + a.get_or_create(&id, ptr)?; + Ok( + a.map_mut(move |x| x.get_mut(&id).unwrap().as_text_mut().unwrap()) + .into(), + ) } - pub fn get_or_create_text_container_mut(&mut self, name: &str) -> ContainerRef { + #[inline(always)] + pub fn get_map_container_mut( + &mut self, + id: &ContainerID, + ) -> Result, LoroError> { let a = OwningRefMut::new(self.container.write()); - a.map_mut(|x| { - x.get_or_create( - &ContainerID::new_root(name, ContainerType::Text), - Irc::downgrade(&self.log_store), - ) - .as_text_mut() - .unwrap() - }) - .into() + Ok( + a.map_mut(move |x| x.get_mut(id).unwrap().as_map_mut().unwrap()) + .into(), + ) } + #[inline(always)] + pub fn get_text_container_mut( + &mut self, + id: &ContainerID, + ) -> Result, LoroError> { + let a = OwningRefMut::new(self.container.write()); + Ok( + a.map_mut(move |x| x.get_mut(id).unwrap().as_text_mut().unwrap()) + .into(), + ) + } + + #[inline(always)] pub fn get_text_container( &self, - name: &str, - ) -> OwningRef, Box> { - let a = OwningRef::new(self.container.write()); - a.map(|x| { - x.get(&ContainerID::new_root(name, ContainerType::Text)) - .unwrap() - .as_text() - .unwrap() - }) + id: &ContainerID, + ) -> Result, LoroError> { + let a = OwningRef::new(self.container.read()); + Ok(a.map(move |x| x.get(id).unwrap().as_text().unwrap()).into()) } pub fn export(&self, remote_vv: VersionVector) -> Vec { diff --git a/crates/loro-core/src/value.rs b/crates/loro-core/src/value.rs index 36e29032..04a6b2fe 100644 --- a/crates/loro-core/src/value.rs +++ b/crates/loro-core/src/value.rs @@ -68,6 +68,8 @@ pub mod wasm { use crate::LoroValue; + use super::InsertValue; + pub fn convert(value: LoroValue) -> JsValue { match value { LoroValue::Null => JsValue::NULL, @@ -103,6 +105,22 @@ pub mod wasm { convert(value) } } + + impl InsertValue { + pub fn try_from_js(value: JsValue) -> Result { + if value.is_null() { + Ok(InsertValue::Null) + } else if value.as_bool().is_some() { + Ok(InsertValue::Bool(value.as_bool().unwrap())) + } else if value.as_f64().is_some() { + Ok(InsertValue::Double(value.as_f64().unwrap())) + } else if value.is_string() { + Ok(InsertValue::String(value.as_string().unwrap().into())) + } else { + Err(value) + } + } + } } #[cfg(test)] diff --git a/crates/loro-core/tests/test.rs b/crates/loro-core/tests/test.rs index 48fe606c..d4981854 100644 --- a/crates/loro-core/tests/test.rs +++ b/crates/loro-core/tests/test.rs @@ -5,7 +5,7 @@ use loro_core::LoroCore; #[test] fn test() { let mut store = LoroCore::new(Default::default(), Some(10)); - let mut text_container = store.get_or_create_text_container_mut("haha".into()); + let mut text_container = store.get_or_create_text_container("haha".into()); text_container.insert(0, "012"); text_container.insert(1, "34"); text_container.insert(1, "56"); @@ -17,7 +17,7 @@ fn test() { let mut store_b = LoroCore::new(Default::default(), Some(11)); let exported = store.export(Default::default()); store_b.import(exported); - let mut text_container = store_b.get_or_create_text_container_mut("haha".into()); + let mut text_container = store_b.get_or_create_text_container("haha".into()); text_container.check(); let value = text_container.get_value(); let value = value.as_string().unwrap(); @@ -31,7 +31,7 @@ fn test() { drop(text_container); store.import(store_b.export(store.vv())); - let mut text_container = store.get_or_create_text_container_mut("haha".into()); + let mut text_container = store.get_or_create_text_container("haha".into()); let value = text_container.get_value(); let value = value.as_string().unwrap(); assert_eq!(value.as_str(), "63417892"); @@ -43,7 +43,7 @@ fn test() { drop(text_container); store_b.import(store.export(Default::default())); - let mut text_container = store_b.get_or_create_text_container_mut("haha".into()); + let mut text_container = store_b.get_or_create_text_container("haha".into()); text_container.check(); let value = text_container.get_value(); let value = value.as_string().unwrap(); diff --git a/crates/loro-wasm/Cargo.toml b/crates/loro-wasm/Cargo.toml index cd5d7cf5..3223ff9a 100644 --- a/crates/loro-wasm/Cargo.toml +++ b/crates/loro-wasm/Cargo.toml @@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] js-sys = "0.3.60" -loro-core = { path = "../loro-core", features = ["wasm"] } +loro-core = { path = "../loro-core", features = ["wasm", "slice"] } wasm-bindgen = "0.2.83" [profile.release] diff --git a/crates/loro-wasm/deno_test/.vscode/settings.json b/crates/loro-wasm/deno_test/.vscode/settings.json new file mode 100644 index 00000000..fc44d7f6 --- /dev/null +++ b/crates/loro-wasm/deno_test/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "deno.enable": true, + "editor.stickyScroll.enabled": false +} diff --git a/crates/loro-wasm/deno_test/test.ts b/crates/loro-wasm/deno_test/test.ts new file mode 100644 index 00000000..720a57a4 --- /dev/null +++ b/crates/loro-wasm/deno_test/test.ts @@ -0,0 +1,10 @@ +import init, { Loro } from "../pkg/loro_wasm.js"; +const wasm = await Deno.readFile("../pkg/loro_wasm_bg.wasm"); + +await init(wasm); +const loro = new Loro(); +const a = loro.get_text_container("ha"); +a.insert(0, "hello world"); +a.delete(6, 5); +a.insert(6, "everyone"); +console.log(a.get_value()); diff --git a/crates/loro-wasm/justfile b/crates/loro-wasm/justfile new file mode 100644 index 00000000..092fb269 --- /dev/null +++ b/crates/loro-wasm/justfile @@ -0,0 +1,2 @@ +build: + wasm-pack build --target web . diff --git a/crates/loro-wasm/src/lib.rs b/crates/loro-wasm/src/lib.rs index b83fb026..5aa43a7a 100644 --- a/crates/loro-wasm/src/lib.rs +++ b/crates/loro-wasm/src/lib.rs @@ -1,32 +1,105 @@ +use std::{ + cell::RefCell, + rc::{Rc, Weak}, +}; + use loro_core::{ - configure::Configure, - container::{manager::ContainerRef, text::text_container::TextContainer as LoroTextContainer}, - LoroCore, + container::{Container, ContainerID}, + InsertValue, LoroCore, LoroError, }; use wasm_bindgen::prelude::*; #[wasm_bindgen] pub struct Loro { - loro: LoroCore, + loro: Rc>, } #[wasm_bindgen] -pub struct TextContainer { - inner: ContainerRef<'static, LoroTextContainer>, +pub struct Text { + loro: Weak>, + id: ContainerID, } +#[wasm_bindgen] +pub struct Map { + loro: Weak>, + id: ContainerID, +} + +#[wasm_bindgen] impl Loro { + #[wasm_bindgen(constructor)] pub fn new() -> Self { Loro { // TODO: expose the configuration - loro: LoroCore::default(), + loro: Rc::new(RefCell::new(LoroCore::default())), } } - pub fn get_text_container(&mut self, name: &str) -> TextContainer { - TextContainer { - inner: self.loro.get_or_create_text_container_mut(name), - } + pub fn get_text_container(&mut self, name: &str) -> Result { + let mut loro = self.loro.borrow_mut(); + let text_container = loro.get_or_create_text_container(name)?; + Ok(Text { + id: text_container.id().clone(), + loro: Rc::downgrade(&self.loro), + }) + } + + pub fn get_map_container(&mut self, name: &str) -> Result { + let mut loro = self.loro.borrow_mut(); + let map = loro.get_or_create_map_container(name)?; + Ok(Map { + id: map.id().clone(), + loro: Rc::downgrade(&self.loro), + }) + } +} + +#[wasm_bindgen] +impl Map { + pub fn set(&mut self, key: String, value: JsValue) { + let loro = self.loro.upgrade().unwrap(); + let mut loro = loro.borrow_mut(); + let mut map = loro.get_map_container_mut(&self.id).unwrap(); + map.insert(key.into(), InsertValue::try_from_js(value).unwrap()) + } + + pub fn delete(&mut self, key: String) { + let loro = self.loro.upgrade().unwrap(); + let mut loro = loro.borrow_mut(); + let mut map = loro.get_map_container_mut(&self.id).unwrap(); + map.delete(key.into()) + } + + pub fn get_value(&mut self) -> JsValue { + let loro = self.loro.upgrade().unwrap(); + let mut loro = loro.borrow_mut(); + let mut map = loro.get_map_container_mut(&self.id).unwrap(); + map.get_value().clone().into() + } +} + +#[wasm_bindgen] +impl Text { + pub fn insert(&mut self, index: usize, text: &str) { + let loro = self.loro.upgrade().unwrap(); + let mut loro = loro.borrow_mut(); + let mut text_container = loro.get_text_container_mut(&self.id).unwrap(); + text_container.insert(index, text); + } + + pub fn delete(&mut self, index: usize, len: usize) { + let loro = self.loro.upgrade().unwrap(); + let mut loro = loro.borrow_mut(); + let mut text_container = loro.get_text_container_mut(&self.id).unwrap(); + text_container.delete(index, len); + } + + pub fn get_value(&mut self) -> String { + let loro = self.loro.upgrade().unwrap(); + let mut loro = loro.borrow_mut(); + let mut text_container = loro.get_text_container_mut(&self.id).unwrap(); + text_container.get_value().as_string().unwrap().to_string() } }