map and array insert, delete for apply()

This commit is contained in:
Orion Henry 2022-09-02 11:57:08 -05:00
parent b5742315ef
commit 47fa3ae218
8 changed files with 377 additions and 38 deletions

View file

@ -1,2 +1,17 @@
import { Automerge as VanillaAutomerge } from "automerge-types"
export * from "automerge-types"
export { default } from "automerge-types"
export class Automerge extends VanillaAutomerge {
// experimental api can go here
applyPatches(obj: any): any;
// override old methods that return automerge
clone(actor?: string): Automerge;
fork(actor?: string): Automerge;
forkAt(heads: Heads, actor?: string): Automerge;
}
export function create(actor?: Actor): Automerge;
export function load(data: Uint8Array, actor?: Actor): Automerge;

View file

@ -2,13 +2,13 @@ use crate::AutoCommit;
use automerge as am;
use automerge::transaction::Transactable;
use automerge::{Change, ChangeHash, Prop};
use js_sys::{Array, Object, Reflect, Uint8Array};
use js_sys::{Array, Function, Object, Reflect, Uint8Array};
use std::collections::{BTreeSet, HashSet};
use std::fmt::Display;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use crate::{ObjId, ScalarValue, Value};
use crate::{observer::Patch, ObjId, ScalarValue, Value};
pub(crate) struct JS(pub(crate) JsValue);
pub(crate) struct AR(pub(crate) Array);
@ -462,6 +462,12 @@ pub(crate) fn list_to_js_at(doc: &AutoCommit, obj: &ObjId, heads: &[ChangeHash])
array.into()
}
/*
pub(crate) fn export_values<'a, V: Iterator<Item = Value<'a>>>(val: V) -> Array {
val.map(|v| export_value(&v)).collect()
}
*/
pub(crate) fn export_value(val: &Value<'_>) -> JsValue {
match val {
Value::Object(o) if o == &am::ObjType::Map || o == &am::ObjType::Table => {
@ -471,3 +477,99 @@ pub(crate) fn export_value(val: &Value<'_>) -> JsValue {
Value::Scalar(v) => ScalarValue(v.clone()).into(),
}
}
pub(crate) fn apply_patch(obj: JsValue, patch: &Patch) -> Result<JsValue, JsValue> {
apply_patch2(obj, patch, 0)
}
pub(crate) fn apply_patch2(obj: JsValue, patch: &Patch, depth: usize) -> Result<JsValue, JsValue> {
match (js_to_map_seq(&obj)?, patch.path().get(depth)) {
(JsObj::Map(o), Some(Prop::Map(key))) => {
let sub_obj = Reflect::get(&obj, &key.into())?;
let new_value = apply_patch2(sub_obj, patch, depth + 1)?;
let result =
Reflect::construct(&o.constructor(), &Array::new())?.dyn_into::<Object>()?;
let result = Object::assign(&result, &o).into();
Reflect::set(&result, &key.into(), &new_value)?;
Ok(result)
}
(JsObj::Seq(a), Some(Prop::Seq(index))) => {
let index = JsValue::from_f64(*index as f64);
let sub_obj = Reflect::get(&obj, &index)?;
let new_value = apply_patch2(sub_obj, patch, depth + 1)?;
let result = Reflect::construct(&a.constructor(), &a)?;
//web_sys::console::log_2(&format!("NEW VAL {}: ", tmpi).into(), &new_value);
Reflect::set(&result, &index, &new_value)?;
Ok(result)
}
(JsObj::Map(o), None) => {
let result =
Reflect::construct(&o.constructor(), &Array::new())?.dyn_into::<Object>()?;
let result = Object::assign(&result, &o);
match patch {
Patch::PutMap { key, value, .. } => {
let result = result.into();
Reflect::set(&result, &key.into(), &export_value(value))?;
Ok(result)
}
Patch::DeleteMap { key, .. } => {
Reflect::delete_property(&result, &key.into())?;
Ok(result.into())
}
Patch::Insert { .. } => Err(to_js_err("cannot insert into map")),
Patch::DeleteSeq { .. } => Err(to_js_err("cannot splice a map")),
Patch::PutSeq { .. } => Err(to_js_err("cannot array index a map")),
_ => unimplemented!(),
}
}
(JsObj::Seq(a), None) => {
match patch {
Patch::PutSeq { index, value, .. } => {
let result = Reflect::construct(&a.constructor(), &a)?;
Reflect::set(&result, &(*index as f64).into(), &export_value(value))?;
Ok(result)
}
Patch::DeleteSeq { index, .. } => {
let result = &a.dyn_into::<Array>()?;
let mut f = |_, i, _| i != *index as u32;
let result = result.filter(&mut f);
Ok(result.into())
}
Patch::Insert { index, values, .. } => {
let from = Reflect::get(&a.constructor().into(), &"from".into())?
.dyn_into::<Function>()?;
let result = from.call1(&JsValue::undefined(), &a)?.dyn_into::<Array>()?;
// FIXME: should be one function call
for v in values {
result.splice(*index as u32, 0, &export_value(v));
}
Ok(result.into())
}
Patch::DeleteMap { .. } => Err(to_js_err("cannot delete from a seq")),
Patch::PutMap { .. } => Err(to_js_err("cannot set key in seq")),
_ => unimplemented!(),
}
}
(_, _) => Err(to_js_err(format!(
"object/patch missmatch {:?} depth={:?}",
patch, depth
))),
}
}
#[derive(Debug)]
enum JsObj {
Map(Object),
Seq(Array),
}
fn js_to_map_seq(value: &JsValue) -> Result<JsObj, JsValue> {
if let Ok(array) = value.clone().dyn_into::<Array>() {
Ok(JsObj::Seq(array))
} else if let Ok(obj) = value.clone().dyn_into::<Object>() {
Ok(JsObj::Map(obj))
} else {
Err(to_js_err("obj is not Object or Array"))
}
}

View file

@ -44,8 +44,8 @@ mod value;
use observer::Observer;
use interop::{
get_heads, js_get, js_set, list_to_js, list_to_js_at, map_to_js, map_to_js_at, to_js_err,
to_objtype, to_prop, AR, JS,
apply_patch, get_heads, js_get, js_set, list_to_js, list_to_js_at, map_to_js, map_to_js_at,
to_js_err, to_objtype, to_prop, AR, JS,
};
use sync::SyncState;
use value::{datatype, ScalarValue};
@ -426,22 +426,29 @@ impl Automerge {
#[wasm_bindgen(js_name = enablePatches)]
pub fn enable_patches(&mut self, enable: JsValue) -> Result<(), JsValue> {
self.doc.ensure_transaction_closed();
let enable = enable
.as_bool()
.ok_or_else(|| to_js_err("must pass a bool to enable_patches"))?;
self.doc.op_observer.enable(enable);
self.doc.observer().enable(enable);
Ok(())
}
#[wasm_bindgen(js_name = applyPatches)]
pub fn apply_patches(&mut self, mut object: JsValue) -> Result<JsValue, JsValue> {
let patches = self.doc.observer().take_patches();
for p in patches {
object = apply_patch(object, &p)?;
}
Ok(object)
}
#[wasm_bindgen(js_name = popPatches)]
pub fn pop_patches(&mut self) -> Result<Array, JsValue> {
// transactions send out observer updates as they occur, not waiting for them to be
// committed.
// If we pop the patches then we won't be able to revert them.
self.doc.ensure_transaction_closed();
let patches = self.doc.op_observer.take_patches();
let patches = self.doc.observer().take_patches();
let result = Array::new();
for p in patches {
result.push(&p.try_into()?);

View file

@ -21,14 +21,31 @@ impl Observer {
}
self.enabled = enable;
}
fn push(&mut self, patch: Patch) {
if let Some(tail) = self.patches.last_mut() {
if let Some(p) = tail.merge(patch) {
self.patches.push(p)
}
} else {
self.patches.push(patch);
}
}
}
#[derive(Debug, Clone)]
pub(crate) enum Patch {
Put {
PutMap {
obj: ObjId,
path: Vec<Prop>,
prop: Prop,
key: String,
value: Value<'static>,
conflict: bool,
},
PutSeq {
obj: ObjId,
path: Vec<Prop>,
index: usize,
value: Value<'static>,
conflict: bool,
},
@ -36,7 +53,7 @@ pub(crate) enum Patch {
obj: ObjId,
path: Vec<Prop>,
index: usize,
value: Value<'static>,
values: Vec<Value<'static>>,
},
Increment {
obj: ObjId,
@ -44,10 +61,16 @@ pub(crate) enum Patch {
prop: Prop,
value: i64,
},
Delete {
DeleteMap {
obj: ObjId,
path: Vec<Prop>,
prop: Prop,
key: String,
},
DeleteSeq {
obj: ObjId,
path: Vec<Prop>,
index: usize,
length: usize,
},
}
@ -66,7 +89,7 @@ impl OpObserver for Observer {
path,
obj,
index,
value,
values: vec![value],
})
}
}
@ -82,13 +105,23 @@ impl OpObserver for Observer {
if self.enabled {
let path = parents.path().into_iter().map(|p| p.1).collect();
let value = tagged_value.0.to_owned();
self.patches.push(Patch::Put {
path,
obj,
prop,
value,
conflict,
})
let patch = match prop {
Prop::Map(key) => Patch::PutMap {
path,
obj,
key,
value,
conflict,
},
Prop::Seq(index) => Patch::PutSeq {
path,
obj,
index,
value,
conflict,
},
};
self.patches.push(patch);
}
}
@ -114,7 +147,16 @@ impl OpObserver for Observer {
fn delete(&mut self, mut parents: Parents<'_>, obj: ObjId, prop: Prop) {
if self.enabled {
let path = parents.path().into_iter().map(|p| p.1).collect();
self.patches.push(Patch::Delete { path, obj, prop })
let patch = match prop {
Prop::Map(key) => Patch::DeleteMap { path, obj, key },
Prop::Seq(index) => Patch::DeleteSeq {
path,
obj,
index,
length: 1,
},
};
self.patches.push(patch)
}
}
@ -146,35 +188,97 @@ fn export_path(path: &[Prop], end: &Prop) -> Array {
result
}
impl Patch {
pub(crate) fn path(&self) -> &[Prop] {
match &self {
Self::PutMap { path, .. } => path.as_slice(),
Self::PutSeq { path, .. } => path.as_slice(),
Self::Increment { path, .. } => path.as_slice(),
Self::Insert { path, .. } => path.as_slice(),
Self::DeleteMap { path, .. } => path.as_slice(),
Self::DeleteSeq { path, .. } => path.as_slice(),
}
}
fn merge(&mut self, other: Patch) -> Option<Patch> {
match (self, &other) {
(
Self::Insert {
obj, index, values, ..
},
Self::Insert {
obj: o2,
values: v2,
index: i2,
..
},
) if obj == o2 && *index + values.len() == *i2 => {
// TODO - there's a way to do this without the clone im sure
values.extend_from_slice(v2.as_slice());
None
}
_ => Some(other),
}
}
}
impl TryFrom<Patch> for JsValue {
type Error = JsValue;
fn try_from(p: Patch) -> Result<Self, Self::Error> {
let result = Object::new();
match p {
Patch::Put {
Patch::PutMap {
path,
prop,
key,
value,
conflict,
..
} => {
js_set(&result, "action", "put")?;
js_set(&result, "path", export_path(path.as_slice(), &prop))?;
js_set(
&result,
"path",
export_path(path.as_slice(), &Prop::Map(key)),
)?;
js_set(&result, "value", export_value(&value))?;
js_set(&result, "conflict", &JsValue::from_bool(conflict))?;
Ok(result.into())
}
Patch::Insert {
path, index, value, ..
Patch::PutSeq {
path,
index,
value,
conflict,
..
} => {
js_set(&result, "action", "ins")?;
js_set(&result, "action", "put")?;
js_set(
&result,
"path",
export_path(path.as_slice(), &Prop::Seq(index)),
)?;
js_set(&result, "value", export_value(&value))?;
js_set(&result, "conflict", &JsValue::from_bool(conflict))?;
Ok(result.into())
}
Patch::Insert {
path,
index,
values,
..
} => {
js_set(&result, "action", "splice")?;
js_set(
&result,
"path",
export_path(path.as_slice(), &Prop::Seq(index)),
)?;
js_set(
&result,
"values",
values.iter().map(export_value).collect::<Array>(),
)?;
Ok(result.into())
}
Patch::Increment {
@ -185,9 +289,30 @@ impl TryFrom<Patch> for JsValue {
js_set(&result, "value", &JsValue::from_f64(value as f64))?;
Ok(result.into())
}
Patch::Delete { path, prop, .. } => {
Patch::DeleteMap { path, key, .. } => {
js_set(&result, "action", "del")?;
js_set(&result, "path", export_path(path.as_slice(), &prop))?;
js_set(
&result,
"path",
export_path(path.as_slice(), &Prop::Map(key)),
)?;
Ok(result.into())
}
Patch::DeleteSeq {
path,
index,
length,
..
} => {
js_set(&result, "action", "del")?;
js_set(
&result,
"path",
export_path(path.as_slice(), &Prop::Seq(index)),
)?;
if length > 1 {
js_set(&result, "length", length)?;
}
Ok(result.into())
}
}

View file

@ -0,0 +1,85 @@
import { describe, it } from 'mocha';
//@ts-ignore
import assert from 'assert'
//@ts-ignore
import init, { create, load } from '..'
describe('Automerge', () => {
describe('Patch Apply', () => {
it('apply nested sets on maps', () => {
let start : any = { hello: { mellow: { yellow: "world", x: 1 }, y : 2 } }
let doc1 = create()
doc1.putObject("/", "hello", start.hello);
let mat = doc1.materialize("/")
let doc2 = create()
doc2.enablePatches(true)
doc2.merge(doc1)
let base = doc2.applyPatches({})
assert.deepEqual(mat, start)
assert.deepEqual(base, start)
doc2.delete("/hello/mellow", "yellow");
delete start.hello.mellow.yellow;
base = doc2.applyPatches(base)
mat = doc2.materialize("/")
assert.deepEqual(mat, start)
assert.deepEqual(base, start)
})
it('apply patches on lists', () => {
//let start = { list: [1,2,3,4,5,6] }
let start = { list: [1,2,3,4] }
let doc1 = create()
doc1.putObject("/", "list", start.list);
let mat = doc1.materialize("/")
let doc2 = create()
doc2.enablePatches(true)
doc2.merge(doc1)
mat = doc1.materialize("/")
let base = doc2.applyPatches({})
assert.deepEqual(mat, start)
assert.deepEqual(base, start)
doc2.delete("/list", 3);
start.list.splice(3,1)
base = doc2.applyPatches(base)
assert.deepEqual(base, start)
})
it('apply patches on lists of lists of lists', () => {
let start = { list:
[
[
[ 1, 2, 3, 4, 5, 6],
[ 7, 8, 9,10,11,12],
],
[
[ 7, 8, 9,10,11,12],
[ 1, 2, 3, 4, 5, 6],
]
]
}
let doc1 = create()
doc1.enablePatches(true)
doc1.putObject("/", "list", start.list);
let mat = doc1.materialize("/")
let base = doc1.applyPatches({})
assert.deepEqual(mat, start)
doc1.delete("/list/0/1", 3)
start.list[0][1].splice(3,1)
doc1.delete("/list/0", 0)
start.list[0].splice(0,1)
mat = doc1.materialize("/")
base = doc1.applyPatches(base)
assert.deepEqual(mat, start)
assert.deepEqual(base, start)
})
})
})

View file

@ -548,8 +548,8 @@ describe('Automerge', () => {
doc2.loadIncremental(doc1.saveIncremental())
assert.deepEqual(doc2.popPatches(), [
{ action: 'put', path: [ 'birds' ], value: [], conflict: false },
{ action: 'ins', path: [ 'birds', 0 ], value: 'Goldfinch' },
{ action: 'ins', path: [ 'birds', 1 ], value: 'Chaffinch' }
{ action: 'splice', path: [ 'birds', 0 ], values: ['Goldfinch'] },
{ action: 'splice', path: [ 'birds', 1 ], values: ['Chaffinch'] }
])
doc1.free()
doc2.free()
@ -563,7 +563,7 @@ describe('Automerge', () => {
doc2.enablePatches(true)
doc2.loadIncremental(doc1.saveIncremental())
assert.deepEqual(doc2.popPatches(), [
{ action: 'ins', path: [ 'birds', 0 ], value: {} },
{ action: 'splice', path: [ 'birds', 0 ], values: [{}] },
{ action: 'put', path: [ 'birds', 0, 'species' ], value: 'Goldfinch', conflict: false },
{ action: 'put', path: [ 'birds', 0, 'count', ], value: 3, conflict: false }
])

View file

@ -16,7 +16,7 @@ use crate::{
pub struct AutoCommitWithObs<Obs: OpObserver> {
doc: Automerge,
transaction: Option<(Obs, TransactionInner)>,
pub op_observer: Obs,
op_observer: Obs,
}
pub type AutoCommit = AutoCommitWithObs<()>;
@ -43,6 +43,11 @@ impl AutoCommit {
}
impl<Obs: OpObserver> AutoCommitWithObs<Obs> {
pub fn observer(&mut self) -> &mut Obs {
self.ensure_transaction_closed();
&mut self.op_observer
}
pub fn with_observer<Obs2: OpObserver>(self, op_observer: Obs2) -> AutoCommitWithObs<Obs2> {
AutoCommitWithObs {
doc: self.doc,
@ -98,7 +103,7 @@ impl<Obs: OpObserver> AutoCommitWithObs<Obs> {
})
}
pub fn ensure_transaction_closed(&mut self) {
fn ensure_transaction_closed(&mut self) {
if let Some((current, tx)) = self.transaction.take() {
self.op_observer.merge(&current);
tx.commit(&mut self.doc, None, None);

View file

@ -1441,7 +1441,7 @@ fn observe_counter_change_application_overwrite() {
doc3.merge(&mut doc2).unwrap();
assert_eq!(
doc3.op_observer.take_patches(),
doc3.observer().take_patches(),
vec![Patch::Put {
obj: ExId::Root,
path: vec![],
@ -1458,7 +1458,7 @@ fn observe_counter_change_application_overwrite() {
doc4.merge(&mut doc1).unwrap();
// no patches as the increments operate on an invisible counter
assert_eq!(doc4.op_observer.take_patches(), vec![]);
assert_eq!(doc4.observer().take_patches(), vec![]);
}
#[test]
@ -1472,7 +1472,7 @@ fn observe_counter_change_application() {
let mut new_doc = AutoCommit::new().with_observer(VecOpObserver::default());
new_doc.apply_changes(changes).unwrap();
assert_eq!(
new_doc.op_observer.take_patches(),
new_doc.observer().take_patches(),
vec![
Patch::Put {
obj: ExId::Root,