feat: basic wasm interface

This commit is contained in:
Zixuan Chen 2022-10-31 12:22:07 +08:00
parent 3a0c00fdec
commit e0a472fd1a
16 changed files with 263 additions and 96 deletions

View file

@ -2,6 +2,7 @@
"recommendations": [
"skellock.just",
"Gruntfuggly.todo-tree",
"rust-lang.rust-analyzer"
"rust-lang.rust-analyzer",
"denoland.vscode-deno"
]
}

View file

@ -19,5 +19,6 @@
"explorer.fileNesting.patterns": {
"*.rs": "${capture}.excalidraw"
},
"excalidraw.theme": "dark"
"excalidraw.theme": "dark",
"deno.enable": true
}

View file

@ -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<IsoRefMut<'a, ContainerManager>, Box<T>>,
}
impl<'a, T> From<OwningRefMut<IsoRefMut<'a, ContainerManager>, Box<T>>> for ContainerRef<'a, T> {
pub struct ContainerRef<'a, T> {
value: OwningRef<IsoRef<'a, ContainerManager>, Box<T>>,
}
impl<'a, T> From<OwningRefMut<IsoRefMut<'a, ContainerManager>, Box<T>>> for ContainerRefMut<'a, T> {
fn from(value: OwningRefMut<IsoRefMut<'a, ContainerManager>, Box<T>>) -> Self {
ContainerRefMut { value }
}
}
impl<'a, T> From<OwningRef<IsoRef<'a, ContainerManager>, Box<T>>> for ContainerRef<'a, T> {
fn from(value: OwningRef<IsoRef<'a, ContainerManager>, Box<T>>) -> 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()
}

View file

@ -22,6 +22,7 @@ pub struct MapContainer {
id: ContainerID,
state: FxHashMap<InternalString, ValueSlot>,
value: Option<LoroValue>,
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);
}
}

View file

@ -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<LoroError> for JsValue {
fn from(value: LoroError) -> Self {
JsValue::from_str(&value.to_string())
}
}
}

View file

@ -177,12 +177,14 @@ impl Actionable for Vec<LoroCore> {
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<LoroCore> {
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<LoroCore> {
}
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<LoroCore> {
}
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<Action>) {
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

View file

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

View file

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

View file

@ -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<IsoRefMut<ContainerManager>, 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<ContainerRefMut<MapContainer>, 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<IsoRefMut<ContainerManager>, Box<MapContainer>> {
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<ContainerRefMut<TextContainer>, 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<TextContainer> {
#[inline(always)]
pub fn get_map_container_mut(
&mut self,
id: &ContainerID,
) -> Result<ContainerRefMut<MapContainer>, 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<ContainerRefMut<TextContainer>, 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<IsoRefMut<ContainerManager>, Box<TextContainer>> {
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<ContainerRef<TextContainer>, 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<Change> {

View file

@ -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<InsertValue, JsValue> {
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)]

View file

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

View file

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

View file

@ -0,0 +1,4 @@
{
"deno.enable": true,
"editor.stickyScroll.enabled": false
}

View file

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

View file

@ -0,0 +1,2 @@
build:
wasm-pack build --target web .

View file

@ -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<RefCell<LoroCore>>,
}
#[wasm_bindgen]
pub struct TextContainer {
inner: ContainerRef<'static, LoroTextContainer>,
pub struct Text {
loro: Weak<RefCell<LoroCore>>,
id: ContainerID,
}
#[wasm_bindgen]
pub struct Map {
loro: Weak<RefCell<LoroCore>>,
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<Text, JsValue> {
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<Map, JsValue> {
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()
}
}