mirror of
https://github.com/loro-dev/loro.git
synced 2025-01-22 12:57:20 +00:00
feat(wasm): add diff, applyDiff, revertTo
This commit is contained in:
parent
43a14ddaca
commit
875b455fc1
7 changed files with 510 additions and 17 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -1480,14 +1480,18 @@ checksum = "3f3d053a135388e6b1df14e8af1212af5064746e9b87a06a345a7a779ee9695a"
|
|||
name = "loro-wasm"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"console_error_panic_hook",
|
||||
"getrandom",
|
||||
"js-sys",
|
||||
"loro-common 1.2.7",
|
||||
"loro-delta 1.2.7",
|
||||
"loro-internal 1.2.7",
|
||||
"loro-rle 1.2.7",
|
||||
"serde",
|
||||
"serde-wasm-bindgen",
|
||||
"serde_json",
|
||||
"smallvec",
|
||||
"tracing",
|
||||
"tracing-wasm",
|
||||
"wasm-bindgen",
|
||||
|
|
|
@ -5,12 +5,9 @@ use crate::{
|
|||
handler::ValueOrHandler,
|
||||
utils::string_slice::StringSlice,
|
||||
};
|
||||
|
||||
use crate::state::TreeParentId;
|
||||
use fractional_index::FractionalIndex;
|
||||
use generic_btree::rle::HasLength;
|
||||
use loro_common::ContainerType;
|
||||
pub use loro_common::LoroValue;
|
||||
use loro_common::{ContainerType, TreeID};
|
||||
|
||||
// TODO: rename this trait
|
||||
pub trait ToJson {
|
||||
|
@ -532,8 +529,8 @@ pub mod wasm {
|
|||
use fractional_index::FractionalIndex;
|
||||
use generic_btree::rle::HasLength;
|
||||
use js_sys::{Array, Object};
|
||||
use loro_common::TreeID;
|
||||
use wasm_bindgen::{JsValue, __rt::IntoJsResult};
|
||||
use loro_common::{LoroValue, TreeID};
|
||||
use wasm_bindgen::{JsCast, JsValue, __rt::IntoJsResult};
|
||||
|
||||
impl From<Index> for JsValue {
|
||||
fn from(value: Index) -> Self {
|
||||
|
@ -950,4 +947,31 @@ pub mod wasm {
|
|||
obj.into_js_result().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&JsValue> for TextMeta {
|
||||
type Error = JsValue;
|
||||
|
||||
fn try_from(value: &JsValue) -> Result<Self, Self::Error> {
|
||||
if value.is_null() || value.is_undefined() {
|
||||
return Ok(TextMeta::default());
|
||||
}
|
||||
|
||||
let obj = value.dyn_ref::<Object>().ok_or("Expected an object")?;
|
||||
let mut meta = TextMeta::default();
|
||||
|
||||
let entries = Object::entries(obj);
|
||||
for i in 0..entries.length() {
|
||||
let entry = entries.get(i);
|
||||
let entry_arr = entry.dyn_ref::<Array>().ok_or("Expected an array")?;
|
||||
let key = entry_arr
|
||||
.get(0)
|
||||
.as_string()
|
||||
.ok_or("Expected a string key")?;
|
||||
let value = entry_arr.get(1);
|
||||
meta.0.insert(key, LoroValue::from(value));
|
||||
}
|
||||
|
||||
Ok(meta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@ loro-internal = { path = "../loro-internal", features = [
|
|||
"counter",
|
||||
"jsonpath",
|
||||
] }
|
||||
loro-common = { path = "../loro-common" }
|
||||
loro-delta = { path = "../delta" }
|
||||
wasm-bindgen = "=0.2.92"
|
||||
serde-wasm-bindgen = { version = "^0.6.5" }
|
||||
wasm-bindgen-derive = "0.2.1"
|
||||
|
@ -25,6 +27,8 @@ rle = { path = "../rle", package = "loro-rle" }
|
|||
tracing-wasm = "0.2.1"
|
||||
tracing = { version = "0.1", features = ["release_max_level_warn"] }
|
||||
serde_json = "1"
|
||||
smallvec = "1.11.2"
|
||||
arrayvec = "0.7.4"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use js_sys::{Array, Map, Object, Reflect, Uint8Array};
|
||||
use loro_internal::container::ContainerID;
|
||||
use loro_internal::delta::ResolvedMapDelta;
|
||||
use loro_common::{IdLp, LoroListValue, LoroMapValue, LoroValue};
|
||||
use loro_delta::{array_vec, DeltaRopeBuilder};
|
||||
use loro_internal::delta::{ResolvedMapDelta, ResolvedMapValue};
|
||||
use loro_internal::encoding::{ImportBlobMetadata, ImportStatus};
|
||||
use loro_internal::event::Diff;
|
||||
use loro_internal::event::{Diff, ListDeltaMeta, ListDiff, TextDiff, TextMeta};
|
||||
use loro_internal::handler::{Handler, ValueOrHandler};
|
||||
use loro_internal::undo::DiffBatch;
|
||||
use loro_internal::version::VersionRange;
|
||||
use loro_internal::{Counter, CounterSpan, FxHashMap, IdSpan, ListDiffItem, LoroDoc, LoroValue};
|
||||
use loro_internal::StringSlice;
|
||||
use loro_internal::{Counter, CounterSpan, FxHashMap, IdSpan, ListDiffItem, LoroDoc};
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
|
||||
use crate::{
|
||||
|
@ -212,12 +213,12 @@ pub(crate) fn js_diff_to_inner_diff(js: JsValue) -> JsResult<Diff> {
|
|||
match diff_type.as_str() {
|
||||
"text" => {
|
||||
let diff = js_sys::Reflect::get(&obj, &"diff".into())?;
|
||||
let text_diff = loro_internal::wasm::js_value_to_text_diff(diff)?;
|
||||
let text_diff = js_value_to_text_diff(&diff)?;
|
||||
Ok(Diff::Text(text_diff))
|
||||
}
|
||||
"map" => {
|
||||
let updated = js_sys::Reflect::get(&obj, &"updated".into())?;
|
||||
let map_diff = js_to_map_delta(updated)?;
|
||||
let map_diff = js_to_map_delta(&updated)?;
|
||||
Ok(Diff::Map(map_diff))
|
||||
}
|
||||
"counter" => {
|
||||
|
@ -232,7 +233,7 @@ pub(crate) fn js_diff_to_inner_diff(js: JsValue) -> JsResult<Diff> {
|
|||
}
|
||||
"list" => {
|
||||
let diff = js_sys::Reflect::get(&obj, &"diff".into())?;
|
||||
let list_diff = loro_internal::wasm::js_value_to_tree_diff(diff)?;
|
||||
let list_diff = js_value_to_list_diff(&diff)?;
|
||||
Ok(Diff::List(list_diff))
|
||||
}
|
||||
_ => Err(format!("Unknown diff type: {}", diff_type).into()),
|
||||
|
@ -464,3 +465,169 @@ fn id_span_vector_to_js_value(v: VersionRange) -> JsValue {
|
|||
}
|
||||
map.into()
|
||||
}
|
||||
|
||||
pub(crate) fn js_value_to_text_diff(js: &JsValue) -> Result<TextDiff, JsValue> {
|
||||
let arr = js
|
||||
.dyn_ref::<Array>()
|
||||
.ok_or_else(|| JsValue::from_str("Expected an array"))?;
|
||||
let mut builder = DeltaRopeBuilder::new();
|
||||
|
||||
for i in 0..arr.length() {
|
||||
let item = arr.get(i);
|
||||
let obj = item
|
||||
.dyn_ref::<Object>()
|
||||
.ok_or_else(|| JsValue::from_str("Expected an object"))?;
|
||||
|
||||
if let Some(retain) = Reflect::get(&obj, &JsValue::from_str("retain"))?.as_f64() {
|
||||
let len = retain as usize;
|
||||
let js_meta = Reflect::get(&obj, &JsValue::from_str("attributes"))?;
|
||||
let meta = TextMeta::try_from(&js_meta).unwrap_or_default();
|
||||
builder = builder.retain(len, meta);
|
||||
} else if let Some(insert) = Reflect::get(&obj, &JsValue::from_str("insert"))?.as_string() {
|
||||
let js_meta = Reflect::get(&obj, &JsValue::from_str("attributes"))?;
|
||||
let meta = TextMeta::try_from(&js_meta).unwrap_or_default();
|
||||
builder = builder.insert(StringSlice::from(insert), meta);
|
||||
} else if let Some(delete) = Reflect::get(&obj, &JsValue::from_str("delete"))?.as_f64() {
|
||||
let len = delete as usize;
|
||||
builder = builder.delete(len);
|
||||
} else {
|
||||
return Err(JsValue::from_str("Invalid delta item"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(builder.build())
|
||||
}
|
||||
|
||||
pub(crate) fn js_to_map_delta(js: &JsValue) -> Result<ResolvedMapDelta, JsValue> {
|
||||
let obj = js
|
||||
.dyn_ref::<Object>()
|
||||
.ok_or_else(|| JsValue::from_str("Expected an object"))?;
|
||||
let mut delta = ResolvedMapDelta::new();
|
||||
|
||||
let entries = Object::entries(&obj);
|
||||
for i in 0..entries.length() {
|
||||
let entry = entries.get(i);
|
||||
let entry_arr = entry.dyn_ref::<Array>().unwrap();
|
||||
let key = entry_arr.get(0).as_string().unwrap();
|
||||
let value = entry_arr.get(1);
|
||||
|
||||
if value.is_object() && !value.is_null() {
|
||||
let obj = value.dyn_ref::<Object>().unwrap();
|
||||
if let Ok(kind) = Reflect::get(&obj, &JsValue::from_str("kind")) {
|
||||
if kind.is_function() {
|
||||
let container = js_to_container(value.clone().unchecked_into())?;
|
||||
delta = delta.with_entry(
|
||||
key.into(),
|
||||
ResolvedMapValue {
|
||||
idlp: IdLp::new(0, 0),
|
||||
value: Some(ValueOrHandler::Handler(container.to_handler())),
|
||||
},
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
delta = delta.with_entry(
|
||||
key.into(),
|
||||
ResolvedMapValue {
|
||||
idlp: IdLp::new(0, 0),
|
||||
value: Some(ValueOrHandler::Value(js_value_to_loro_value(&value))),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(delta)
|
||||
}
|
||||
|
||||
pub(crate) fn js_value_to_list_diff(js: &JsValue) -> Result<ListDiff, JsValue> {
|
||||
let arr = js
|
||||
.dyn_ref::<Array>()
|
||||
.ok_or_else(|| JsValue::from_str("Expected an array"))?;
|
||||
let mut builder = DeltaRopeBuilder::new();
|
||||
|
||||
for i in 0..arr.length() {
|
||||
let item = arr.get(i);
|
||||
let obj = item
|
||||
.dyn_ref::<Object>()
|
||||
.ok_or_else(|| JsValue::from_str("Expected an object"))?;
|
||||
|
||||
if let Some(retain) = Reflect::get(&obj, &JsValue::from_str("retain"))?.as_f64() {
|
||||
let len = retain as usize;
|
||||
builder = builder.retain(len, ListDeltaMeta::default());
|
||||
} else if let Some(delete) = Reflect::get(&obj, &JsValue::from_str("delete"))?.as_f64() {
|
||||
let len = delete as usize;
|
||||
builder = builder.delete(len);
|
||||
} else if let Ok(insert) = Reflect::get(&obj, &JsValue::from_str("insert")) {
|
||||
let insert_arr = insert
|
||||
.dyn_ref::<Array>()
|
||||
.ok_or_else(|| JsValue::from_str("insert must be an array"))?;
|
||||
let mut values = array_vec::ArrayVec::<ValueOrHandler, 8>::new();
|
||||
|
||||
for j in 0..insert_arr.length() {
|
||||
let value = insert_arr.get(j);
|
||||
if value.is_object() && !value.is_null() {
|
||||
let obj = value.dyn_ref::<Object>().unwrap();
|
||||
if let Ok(kind) = Reflect::get(&obj, &JsValue::from_str("kind")) {
|
||||
if kind.is_function() {
|
||||
let container = js_to_container(value.clone().unchecked_into())?;
|
||||
values
|
||||
.push(ValueOrHandler::Handler(container.to_handler()))
|
||||
.unwrap();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
values
|
||||
.push(ValueOrHandler::Value(js_value_to_loro_value(&value)))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
builder = builder.insert(values, ListDeltaMeta::default());
|
||||
} else {
|
||||
return Err(JsValue::from_str("Invalid delta item"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(builder.build())
|
||||
}
|
||||
|
||||
pub(crate) fn js_value_to_loro_value(js: &JsValue) -> LoroValue {
|
||||
if js.is_null() {
|
||||
LoroValue::Null
|
||||
} else if let Some(b) = js.as_bool() {
|
||||
LoroValue::Bool(b)
|
||||
} else if let Some(n) = js.as_f64() {
|
||||
if n.fract() == 0.0 && n >= -(2i64.pow(53) as f64) && n <= 2i64.pow(53) as f64 {
|
||||
LoroValue::I64(n as i64)
|
||||
} else {
|
||||
LoroValue::Double(n)
|
||||
}
|
||||
} else if let Some(s) = js.as_string() {
|
||||
LoroValue::String(s.into())
|
||||
} else if js.is_array() {
|
||||
let arr = Array::from(js);
|
||||
let mut vec = Vec::with_capacity(arr.length() as usize);
|
||||
for i in 0..arr.length() {
|
||||
vec.push(js_value_to_loro_value(&arr.get(i)));
|
||||
}
|
||||
LoroValue::List(LoroListValue::from(vec))
|
||||
} else if js.is_object() {
|
||||
let obj = Object::from(JsValue::from(js));
|
||||
let mut map = FxHashMap::default();
|
||||
let entries = Object::entries(&obj);
|
||||
for i in 0..entries.length() {
|
||||
let entry = entries.get(i);
|
||||
let key = entry
|
||||
.dyn_ref::<Array>()
|
||||
.unwrap()
|
||||
.get(0)
|
||||
.as_string()
|
||||
.unwrap();
|
||||
let value = entry.dyn_ref::<Array>().unwrap().get(1);
|
||||
map.insert(key, js_value_to_loro_value(&value));
|
||||
}
|
||||
LoroValue::Map(LoroMapValue::from(map))
|
||||
} else {
|
||||
LoroValue::Null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
// #![warn(missing_docs)]
|
||||
|
||||
use convert::{
|
||||
import_status_to_js_value, js_to_id_span, js_to_version_vector, resolved_diff_to_js,
|
||||
import_status_to_js_value, js_diff_to_inner_diff, js_to_id_span, js_to_version_vector,
|
||||
resolved_diff_to_js,
|
||||
};
|
||||
use js_sys::{Array, Object, Promise, Reflect, Uint8Array};
|
||||
use loro_internal::{
|
||||
|
@ -1874,7 +1875,21 @@ impl LoroDoc {
|
|||
.collect())
|
||||
}
|
||||
|
||||
/// Revert the document to the given frontiers
|
||||
/// Revert the document to the given frontiers.
|
||||
///
|
||||
/// The doc will not become detached when using this method. Instead, it will generate a series
|
||||
/// of operations to revert the document to the given version.
|
||||
///
|
||||
/// @example
|
||||
/// ```ts
|
||||
/// const doc = new LoroDoc();
|
||||
/// doc.setPeerId("1");
|
||||
/// const text = doc.getText("text");
|
||||
/// text.insert(0, "Hello");
|
||||
/// doc.commit();
|
||||
/// doc.revertTo([{ peer: "1", counter: 1 }]);
|
||||
/// expect(doc.getText("text").toString()).toBe("He");
|
||||
/// ```
|
||||
#[wasm_bindgen(js_name = "revertTo")]
|
||||
pub fn revert_to(&self, frontiers: Vec<JsID>) -> JsResult<()> {
|
||||
let frontiers = ids_to_frontiers(frontiers)?;
|
||||
|
@ -1882,6 +1897,8 @@ impl LoroDoc {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply a diff batch to the document
|
||||
#[wasm_bindgen(js_name = "applyDiff")]
|
||||
pub fn apply_diff(&self, diff: JsDiffBatch) -> JsResult<()> {
|
||||
let diff: JsValue = diff.into();
|
||||
let obj: js_sys::Object = diff.into();
|
||||
|
@ -1890,7 +1907,14 @@ impl LoroDoc {
|
|||
let entry = entry.unchecked_into::<js_sys::Array>();
|
||||
let k = entry.get(0);
|
||||
let v = entry.get(1);
|
||||
diff.insert(k.as_string().unwrap(), js_diff_to_inner_diff(v));
|
||||
diff.insert(
|
||||
k.as_string()
|
||||
.ok_or("Expected string key")?
|
||||
.as_str()
|
||||
.try_into()
|
||||
.map_err(|_| "Failed to convert key")?,
|
||||
js_diff_to_inner_diff(v)?,
|
||||
);
|
||||
}
|
||||
self.0.apply_diff(DiffBatch(diff))?;
|
||||
Ok(())
|
||||
|
|
203
crates/loro-wasm/tests/__snapshots__/basic.test.ts.snap
Normal file
203
crates/loro-wasm/tests/__snapshots__/basic.test.ts.snap
Normal file
|
@ -0,0 +1,203 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`can diff two versions 1`] = `
|
||||
{
|
||||
"cid:root-list:List": {
|
||||
"diff": [
|
||||
{
|
||||
"insert": [
|
||||
"item1",
|
||||
],
|
||||
},
|
||||
],
|
||||
"type": "list",
|
||||
},
|
||||
"cid:root-map:Map": {
|
||||
"type": "map",
|
||||
"updated": {
|
||||
"key1": "value1",
|
||||
"key2": 42,
|
||||
},
|
||||
},
|
||||
"cid:root-text:Text": {
|
||||
"diff": [
|
||||
{
|
||||
"attributes": {
|
||||
"bold": true,
|
||||
},
|
||||
"insert": "Hello",
|
||||
},
|
||||
],
|
||||
"type": "text",
|
||||
},
|
||||
"cid:root-tree:Tree": {
|
||||
"diff": [
|
||||
{
|
||||
"action": "create",
|
||||
"fractionalIndex": "80",
|
||||
"index": 0,
|
||||
"parent": undefined,
|
||||
"target": "12@1",
|
||||
},
|
||||
{
|
||||
"action": "create",
|
||||
"fractionalIndex": "80",
|
||||
"index": 0,
|
||||
"parent": "12@1",
|
||||
"target": "13@1",
|
||||
},
|
||||
],
|
||||
"type": "tree",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`can diff two versions 2`] = `
|
||||
{
|
||||
"list": [
|
||||
"item1",
|
||||
],
|
||||
"map": {
|
||||
"key1": "value1",
|
||||
"key2": 42,
|
||||
},
|
||||
"text": "Hello",
|
||||
"tree": [
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"fractional_index": "80",
|
||||
"id": "1@2",
|
||||
"index": 0,
|
||||
"meta": {},
|
||||
"parent": "0@2",
|
||||
},
|
||||
],
|
||||
"fractional_index": "80",
|
||||
"id": "0@2",
|
||||
"index": 0,
|
||||
"meta": {},
|
||||
"parent": null,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`the diff will deduplication 1`] = `
|
||||
{
|
||||
"cid:root-hi:Text": {
|
||||
"diff": [
|
||||
{
|
||||
"insert": "Hello",
|
||||
},
|
||||
],
|
||||
"type": "text",
|
||||
},
|
||||
"cid:root-map:Map": {
|
||||
"type": "map",
|
||||
"updated": {
|
||||
"0": null,
|
||||
"1": null,
|
||||
"10": null,
|
||||
"11": null,
|
||||
"12": null,
|
||||
"13": null,
|
||||
"14": null,
|
||||
"15": null,
|
||||
"16": null,
|
||||
"17": null,
|
||||
"18": null,
|
||||
"19": null,
|
||||
"2": null,
|
||||
"20": null,
|
||||
"21": null,
|
||||
"22": null,
|
||||
"23": null,
|
||||
"24": null,
|
||||
"25": null,
|
||||
"26": null,
|
||||
"27": null,
|
||||
"28": null,
|
||||
"29": null,
|
||||
"3": null,
|
||||
"30": null,
|
||||
"31": null,
|
||||
"32": null,
|
||||
"33": null,
|
||||
"34": null,
|
||||
"35": null,
|
||||
"36": null,
|
||||
"37": null,
|
||||
"38": null,
|
||||
"39": null,
|
||||
"4": null,
|
||||
"40": null,
|
||||
"41": null,
|
||||
"42": null,
|
||||
"43": null,
|
||||
"44": null,
|
||||
"45": null,
|
||||
"46": null,
|
||||
"47": null,
|
||||
"48": null,
|
||||
"49": null,
|
||||
"5": null,
|
||||
"50": null,
|
||||
"51": null,
|
||||
"52": null,
|
||||
"53": null,
|
||||
"54": null,
|
||||
"55": null,
|
||||
"56": null,
|
||||
"57": null,
|
||||
"58": null,
|
||||
"59": null,
|
||||
"6": null,
|
||||
"60": null,
|
||||
"61": null,
|
||||
"62": null,
|
||||
"63": null,
|
||||
"64": null,
|
||||
"65": null,
|
||||
"66": null,
|
||||
"67": null,
|
||||
"68": null,
|
||||
"69": null,
|
||||
"7": null,
|
||||
"70": null,
|
||||
"71": null,
|
||||
"72": null,
|
||||
"73": null,
|
||||
"74": null,
|
||||
"75": null,
|
||||
"76": null,
|
||||
"77": null,
|
||||
"78": null,
|
||||
"79": null,
|
||||
"8": null,
|
||||
"80": null,
|
||||
"81": null,
|
||||
"82": null,
|
||||
"83": null,
|
||||
"84": null,
|
||||
"85": null,
|
||||
"86": null,
|
||||
"87": null,
|
||||
"88": null,
|
||||
"89": null,
|
||||
"9": null,
|
||||
"90": null,
|
||||
"91": null,
|
||||
"92": null,
|
||||
"93": null,
|
||||
"94": null,
|
||||
"95": null,
|
||||
"96": null,
|
||||
"97": null,
|
||||
"98": null,
|
||||
"99": null,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
|
@ -1151,3 +1151,70 @@ it("can travel changes from event", async () => {
|
|||
await Promise.resolve();
|
||||
expect(done).toBe(true);
|
||||
})
|
||||
|
||||
it("can revert to frontiers", () => {
|
||||
const doc = new LoroDoc();
|
||||
doc.setPeerId("1");
|
||||
doc.getText("text").update("Hello");
|
||||
doc.commit();
|
||||
doc.revertTo([{ peer: "1", counter: 1 }]);
|
||||
expect(doc.getText("text").toString()).toBe("He");
|
||||
})
|
||||
|
||||
it("can diff two versions", () => {
|
||||
const doc = new LoroDoc();
|
||||
doc.setPeerId("1");
|
||||
// Text edits with formatting
|
||||
const text = doc.getText("text");
|
||||
text.update("Hello");
|
||||
text.mark({ start: 0, end: 5 }, "bold", true);
|
||||
doc.commit();
|
||||
|
||||
// Map edits
|
||||
const map = doc.getMap("map");
|
||||
map.set("key1", "value1");
|
||||
map.set("key2", 42);
|
||||
doc.commit();
|
||||
|
||||
// List edits
|
||||
const list = doc.getList("list");
|
||||
list.insert(0, "item1");
|
||||
list.insert(1, "item2");
|
||||
list.delete(1, 1);
|
||||
doc.commit();
|
||||
|
||||
// Tree edits
|
||||
const tree = doc.getTree("tree");
|
||||
const a = tree.createNode();
|
||||
a.createNode();
|
||||
doc.commit();
|
||||
|
||||
const diff = doc.diff([], doc.frontiers());
|
||||
expect(diff).toMatchSnapshot()
|
||||
|
||||
const doc2 = new LoroDoc();
|
||||
doc2.setPeerId("2");
|
||||
doc2.applyDiff(diff);
|
||||
expect(doc2.toJSON()).toMatchSnapshot()
|
||||
expect(doc2.getText("text").toDelta()).toStrictEqual(doc.getText("text").toDelta())
|
||||
})
|
||||
|
||||
it('the diff will deduplication', () => {
|
||||
const doc = new LoroDoc();
|
||||
const list = doc.getList("list");
|
||||
const map = doc.getMap("map");
|
||||
doc.getText("hi").insert(0, "Hello");
|
||||
for (let i = 0; i < 100; i += 1) {
|
||||
list.push(1)
|
||||
map.set(i.toString(), i);
|
||||
doc.setNextCommitMessage("hi " + i);
|
||||
doc.commit();
|
||||
}
|
||||
|
||||
list.clear();
|
||||
map.clear();
|
||||
doc.commit();
|
||||
|
||||
const diff = doc.diff([], doc.frontiers());
|
||||
expect(diff).toMatchSnapshot()
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue