(WASM )Refactor wasm interfaces on List and Map (#192)

* refactor: refine wasm interfaces

* docs: update wasm doc
This commit is contained in:
Zixuan Chen 2023-11-27 17:53:02 +08:00 committed by GitHub
parent e23ef4362d
commit 88218c10bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 211 additions and 118 deletions

View file

@ -6,7 +6,7 @@ use crate::{
richtext::TextStyleInfoFlag,
tree::tree_op::TreeOp,
},
delta::{MapValue, TreeDiffItem, TreeExternalDiff},
delta::{TreeDiffItem, TreeExternalDiff},
op::ListSlice,
state::RichtextState,
txn::EventHint,
@ -790,19 +790,29 @@ impl ListHandler {
pub fn for_each<I>(&self, mut f: I)
where
I: FnMut(&LoroValue),
I: FnMut(ValueOrContainer),
{
self.state
.upgrade()
.unwrap()
.lock()
.unwrap()
.with_state(self.container_idx, |state| {
let a = state.as_list_state().unwrap();
for v in a.iter() {
f(v);
let mutex = &self.state.upgrade().unwrap();
let doc_state = &mutex.lock().unwrap();
let arena = doc_state.arena.clone();
doc_state.with_state(self.container_idx, |state| {
let a = state.as_list_state().unwrap();
for v in a.iter() {
match v {
LoroValue::Container(c) => {
let idx = arena.register_container(c);
f(ValueOrContainer::Container(Handler::new(
self.txn.clone(),
idx,
self.state.clone(),
)));
}
value => {
f(ValueOrContainer::Value(value.clone()));
}
}
})
}
})
}
}
@ -904,19 +914,33 @@ impl MapHandler {
pub fn for_each<I>(&self, mut f: I)
where
I: FnMut(&str, &MapValue),
I: FnMut(&str, ValueOrContainer),
{
self.state
.upgrade()
.unwrap()
.lock()
.unwrap()
.with_state(self.container_idx, |state| {
let a = state.as_map_state().unwrap();
for (k, v) in a.iter() {
f(k, v);
let mutex = &self.state.upgrade().unwrap();
let doc_state = mutex.lock().unwrap();
let arena = doc_state.arena.clone();
doc_state.with_state(self.container_idx, |state| {
let a = state.as_map_state().unwrap();
for (k, v) in a.iter() {
match &v.value {
Some(v) => match v {
LoroValue::Container(c) => {
let idx = arena.register_container(c);
f(
k,
ValueOrContainer::Container(Handler::new(
self.txn.clone(),
idx,
self.state.clone(),
)),
)
}
value => f(k, ValueOrContainer::Value(value.clone())),
},
None => {}
}
})
}
})
}
pub fn get_value(&self) -> LoroValue {

View file

@ -86,6 +86,12 @@ extern "C" {
pub type JsChange;
#[wasm_bindgen(typescript_type = "Map<bigint, number> | Uint8Array")]
pub type JsVersionVector;
#[wasm_bindgen(typescript_type = "Value | Container")]
pub type JsValueOrContainer;
#[wasm_bindgen(typescript_type = "Value | Container | undefined")]
pub type JsValueOrContainerOrUndefined;
#[wasm_bindgen(typescript_type = "[string, Value | Container]")]
pub type MapEntry;
}
mod observer {
@ -885,13 +891,6 @@ impl Loro {
.vv_to_frontiers(&vv);
Ok(frontiers_to_ids(&f))
}
/// same as `toJson`
#[wasm_bindgen(js_name = "getDeepValue")]
pub fn get_deep_value(&self) -> JsValue {
let value = self.0.borrow_mut().get_deep_value();
JsValue::from(value)
}
}
fn js_map_to_vv(map: js_sys::Map) -> JsResult<VersionVector> {
@ -1027,6 +1026,11 @@ struct MarkRange {
#[wasm_bindgen]
impl LoroText {
/// "Text"
pub fn kind(&self) -> JsValue {
JsValue::from_str("Text")
}
/// Insert some string at index.
///
/// @example
@ -1251,6 +1255,11 @@ const CONTAINER_TYPE_ERR: &str = "Invalid container type, only supports Text, Ma
#[wasm_bindgen]
impl LoroMap {
/// "Map"
pub fn kind(&self) -> JsValue {
JsValue::from_str("Map")
}
/// Set the key with the value.
///
/// If the value of the key is exist, the old value will be updated.
@ -1286,7 +1295,8 @@ impl LoroMap {
Ok(())
}
/// Get the value of the key. If the value is a container, the corresponding handler will be returned.
/// Get the value of the key. If the value is a child container, the corresponding
/// `Container` will be returned.
///
/// @example
/// ```ts
@ -1297,13 +1307,14 @@ impl LoroMap {
/// map.set("foo", "bar");
/// const bar = map.get("foo");
/// ```
pub fn get(&self, key: &str) -> JsValue {
pub fn get(&self, key: &str) -> JsValueOrContainerOrUndefined {
let v = self.handler.get_(key);
match v {
(match v {
Some(ValueOrContainer::Container(c)) => handler_to_js_value(c, self.doc.clone()),
Some(ValueOrContainer::Value(v)) => v.into(),
None => JsValue::UNDEFINED,
}
})
.into()
}
/// Get the keys of the map.
@ -1320,15 +1331,14 @@ impl LoroMap {
/// ```
pub fn keys(&self) -> Vec<JsValue> {
let mut ans = Vec::with_capacity(self.handler.len());
self.handler.for_each(|k, v| {
if v.value.is_some() {
ans.push(k.to_string().into());
}
self.handler.for_each(|k, _| {
ans.push(k.to_string().into());
});
ans
}
/// Get the values of the map.
/// Get the values of the map. If the value is a child container, the corresponding
/// `Container` will be returned.
///
/// @example
/// ```ts
@ -1343,14 +1353,13 @@ impl LoroMap {
pub fn values(&self) -> Vec<JsValue> {
let mut ans: Vec<JsValue> = Vec::with_capacity(self.handler.len());
self.handler.for_each(|_, v| {
if let Some(v) = &v.value {
ans.push(v.clone().into());
}
ans.push(loro_value_to_js_value_or_container(v, &self.doc));
});
ans
}
/// Get the entries of the map.
/// Get the entries of the map. If the value is a child container, the corresponding
/// `Container` will be returned.
///
/// @example
/// ```ts
@ -1362,40 +1371,18 @@ impl LoroMap {
/// map.set("baz", "bar");
/// const entries = map.entries(); // [["foo", "bar"], ["baz", "bar"]]
/// ```
pub fn entries(&self) -> Vec<JsValue> {
let mut ans: Vec<JsValue> = Vec::with_capacity(self.handler.len());
pub fn entries(&self) -> Vec<MapEntry> {
let mut ans: Vec<MapEntry> = Vec::with_capacity(self.handler.len());
self.handler.for_each(|k, v| {
if let Some(v) = &v.value {
let array = Array::new();
array.push(&k.to_string().into());
array.push(&v.clone().into());
ans.push(array.into());
}
let array = Array::new();
array.push(&k.to_string().into());
array.push(&loro_value_to_js_value_or_container(v, &self.doc));
let v: JsValue = array.into();
ans.push(v.into());
});
ans
}
/// Get the keys and values shallowly
///
/// {@link LoroMap.getDeepValue}
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const map = doc.getMap("map");
/// map.set("foo", "bar");
/// const text = map.setContainer("text", "Text");
/// text.insert(0, "Hello");
/// console.log(map.value); // {foo: "bar", text: "cid:1@74CAF43A01FF0725:Text"}
/// ```
#[wasm_bindgen(js_name = "value", method, getter)]
pub fn get_value(&self) -> JsValue {
let value = self.handler.get_value();
value.into()
}
/// The container id of this handler.
#[wasm_bindgen(js_name = "id", method, getter)]
pub fn id(&self) -> JsContainerID {
@ -1403,8 +1390,8 @@ impl LoroMap {
value.into()
}
/// Get the keys and the values. If the type of value is a container, it will be
/// resolved recursively.
/// Get the keys and the values. If the type of value is a child container,
/// it will be resolved recursively.
///
/// @example
/// ```ts
@ -1417,8 +1404,8 @@ impl LoroMap {
/// text.insert(0, "Hello");
/// console.log(map.getDeepValue()); // {"foo": "bar", "text": "Hello"}
/// ```
#[wasm_bindgen(js_name = "getDeepValue")]
pub fn get_value_deep(&self) -> JsValue {
#[wasm_bindgen(js_name = "toJson")]
pub fn to_json(&self) -> JsValue {
self.handler.get_deep_value().into()
}
@ -1561,6 +1548,11 @@ pub struct LoroList {
#[wasm_bindgen]
impl LoroList {
/// "List"
pub fn kind(&self) -> JsValue {
JsValue::from_str("List")
}
/// Insert a value at index.
///
/// @example
@ -1608,15 +1600,16 @@ impl LoroList {
/// console.log(list.get(0)); // 100
/// console.log(list.get(1)); // undefined
/// ```
pub fn get(&self, index: usize) -> JsValue {
pub fn get(&self, index: usize) -> JsValueOrContainerOrUndefined {
let Some(v) = self.handler.get_(index) else {
return JsValue::UNDEFINED;
return JsValue::UNDEFINED.into();
};
match v {
(match v {
ValueOrContainer::Value(v) => v.into(),
ValueOrContainer::Container(h) => handler_to_js_value(h, self.doc.clone()),
}
})
.into()
}
/// Get the id of this container.
@ -1626,7 +1619,8 @@ impl LoroList {
value.into()
}
/// Get elements of the list.
/// Get elements of the list. If the value is a child container, the corresponding
/// `Container` will be returned.
///
/// @example
/// ```ts
@ -1637,11 +1631,25 @@ impl LoroList {
/// list.insert(0, 100);
/// list.insert(1, "foo");
/// list.insert(2, true);
/// console.log(list.value); // [100, "foo", true];
/// list.insertContainer(3, "Text");
/// console.log(list.value); // [100, "foo", true, LoroText];
/// ```
#[wasm_bindgen(js_name = "value", method, getter)]
pub fn get_value(&mut self) -> JsValue {
self.handler.get_value().into()
#[wasm_bindgen(js_name = "toArray", method)]
pub fn to_array(&mut self) -> Vec<JsValueOrContainer> {
let mut arr: Vec<JsValueOrContainer> = Vec::with_capacity(self.length());
self.handler.for_each(|x| {
arr.push(match x {
ValueOrContainer::Value(v) => {
let v: JsValue = v.into();
v.into()
}
ValueOrContainer::Container(h) => {
let v: JsValue = handler_to_js_value(h, self.doc.clone());
v.into()
}
});
});
arr
}
/// Get elements of the list. If the type of a element is a container, it will be
@ -1658,8 +1666,8 @@ impl LoroList {
/// text.insert(0, "Hello");
/// console.log(list.getDeepValue()); // [100, "Hello"];
/// ```
#[wasm_bindgen(js_name = "getDeepValue")]
pub fn get_deep_value(&self) -> JsValue {
#[wasm_bindgen(js_name = "toJson")]
pub fn to_json(&self) -> JsValue {
let value = self.handler.get_deep_value();
value.into()
}
@ -1789,6 +1797,11 @@ pub struct LoroTree {
#[wasm_bindgen]
impl LoroTree {
/// "Tree"
pub fn kind(&self) -> JsValue {
JsValue::from_str("Tree")
}
/// Create a new tree node as the child of parent and return an unique tree id.
/// If the parent is undefined, the tree node will be a root node.
///
@ -1970,8 +1983,8 @@ impl LoroTree {
/// // [ { id: '0@F2462C4159C4C8D1', parent: null, meta: { color: 'red' } } ]
/// console.log(tree.getDeepValue());
/// ```
#[wasm_bindgen(js_name = "getDeepValue")]
pub fn get_value_deep(&self) -> JsValue {
#[wasm_bindgen(js_name = "toJson")]
pub fn to_json(&self) -> JsValue {
self.handler.get_deep_value().into()
}
@ -2139,6 +2152,22 @@ fn vv_to_js_value(vv: VersionVector) -> JsValue {
map.into()
}
fn loro_value_to_js_value_or_container(
value: ValueOrContainer,
doc: &Rc<RefCell<LoroDoc>>,
) -> JsValue {
match value {
ValueOrContainer::Value(v) => {
let value: JsValue = v.into();
value
}
ValueOrContainer::Container(c) => {
let handler: JsValue = handler_to_js_value(c, doc.clone());
handler
}
}
}
#[wasm_bindgen(typescript_custom_section)]
const TYPES: &'static str = r#"
/**
@ -2234,4 +2263,20 @@ export interface Change {
length: number,
deps: OpId[],
}
/**
* Data types supported by loro
*/
export type Value =
| ContainerID
| string
| number
| boolean
| null
| { [key: string]: Value }
| Uint8Array
| Value[];
export type Container = LoroList | LoroMap | LoroText | LoroTree;
"#;

View file

@ -1,13 +1,11 @@
export * from "loro-wasm";
import { Delta, OpId } from "loro-wasm";
import { Container, ContainerType, Delta, OpId, Value } from "loro-wasm";
import { PrelimText, PrelimList, PrelimMap } from "loro-wasm";
import {
ContainerID,
Loro,
LoroList,
LoroMap,
LoroText,
LoroTree,
TreeID,
} from "loro-wasm";
@ -40,20 +38,6 @@ LoroMap.prototype.setTyped = function (...args) {
return this.set(...args);
};
/**
* Data types supported by loro
*/
export type Value =
| ContainerID
| string
| number
| boolean
| null
| { [key: string]: Value }
| Uint8Array
| Value[];
export type Container = LoroList | LoroMap | LoroText | LoroTree;
export type Prelim = PrelimList | PrelimMap | PrelimText;
export type Frontiers = OpId[];
@ -129,6 +113,23 @@ export function isContainerId(s: string): s is ContainerID {
export { Loro };
export function isContainer(value: any): value is Container {
if (typeof value !== "object" || value == null) {
return false;
}
const p = value.__proto__;
return p.hasOwnProperty("kind") && CONTAINER_TYPES.includes(value.kind());
}
export function valueType(value: any): "Json" | ContainerType {
if (isContainer(value)) {
return value.kind();
}
return "Json";
}
declare module "loro-wasm" {
interface Loro {
subscribe(listener: Listener): number;

View file

@ -1,12 +1,14 @@
import { describe, expect, it } from "vitest";
import {
ContainerID,
Loro,
LoroList,
LoroMap,
isContainer,
setPanicHook,
toEncodedVersion,
valueType,
} from "../src";
import { Container } from "../dist/loro";
setPanicHook();
@ -79,7 +81,7 @@ it("basic sync example", () => {
it("basic events", () => {
const doc = new Loro();
doc.subscribe((event) => {});
doc.subscribe((event) => { });
const list = doc.getList("list");
});
@ -92,10 +94,21 @@ describe("list", () => {
const v = list.get(0) as LoroMap;
console.log(v);
expect(v instanceof LoroMap).toBeTruthy();
expect(v.getDeepValue()).toStrictEqual({ key: "value" });
expect(v.toJson()).toStrictEqual({ key: "value" });
});
it.todo("iterate");
it("toArray", () => {
const doc = new Loro();
const list = doc.getList("list");
list.insert(0, 1);
list.insert(1, 2);
expect(list.toArray()).toStrictEqual([1, 2]);
list.insertContainer(2, "Text");
const t = list.toArray()[2];
expect(isContainer(t)).toBeTruthy();
expect(valueType(t)).toBe("Text");
expect(valueType(123)).toBe("Json");
});
});
describe("map", () => {
@ -105,7 +118,7 @@ describe("map", () => {
const list = map.setContainer("key", "List");
list.insert(0, 1);
expect(map.get("key") instanceof LoroList).toBeTruthy();
expect((map.get("key") as LoroList).getDeepValue()).toStrictEqual([1]);
expect((map.get("key") as LoroList).toJson()).toStrictEqual([1]);
});
it("set large int", () => {
@ -203,6 +216,15 @@ describe("map", () => {
["baz", "bar"],
]);
});
it("entries should return container handlers", () => {
const doc = new Loro();
const map = doc.getMap("map");
map.setContainer("text", "Text");
map.set("foo", "bar");
const entries = map.entries();
expect((entries[0][1]! as Container).kind() === "Text").toBeTruthy();
})
});
it("handlers should still be usable after doc is dropped", () => {
@ -214,7 +236,8 @@ it("handlers should still be usable after doc is dropped", () => {
text.insert(0, "123");
expect(text.toString()).toBe("123");
list.insert(0, 1);
expect(list.getDeepValue()).toStrictEqual([1]);
expect(list.toJson()).toStrictEqual([1]);
map.set("k", 8);
expect(map.getDeepValue()).toStrictEqual({ k: 8 });
expect(map.toJson()).toStrictEqual({ k: 8 });
});

View file

@ -218,7 +218,7 @@ describe("prelim", () => {
map.set("list", prelim_list);
loro.commit();
assertEquals(map.getDeepValue(), {
assertEquals(map.toJson(), {
text: "hello everyone",
map: { a: 1, ab: 123 },
list: [0, { a: 4 }],
@ -234,7 +234,7 @@ describe("prelim", () => {
list.insert(2, prelim_list);
loro.commit();
assertEquals(list.getDeepValue(), [
assertEquals(list.toJson(), [
"ttt",
{ a: 1, b: 2 },
[
@ -270,17 +270,17 @@ describe("wasm", () => {
it("getValueDeep", () => {
bText.insert(0, "hello world Text");
assertEquals(b.getDeepValue(), { ab: 123, hh: "hello world Text" });
assertEquals(b.toJson(), { ab: 123, hh: "hello world Text" });
});
it("get container by id", () => {
const id = b.id;
const b2 = loro.getContainerById(id) as LoroMap;
assertEquals(b2.value, b.value);
assertEquals(b2.toJson(), b.toJson());
assertEquals(b2.id, id);
b2.set("0", 12);
assertEquals(b2.value, b.value);
assertEquals(b2.toJson(), b.toJson());
});
});