Compare commits

...

16 commits
main ... blame

Author SHA1 Message Date
Orion Henry
c1be06a6c7 blame wip 1 2022-02-28 19:02:36 -05:00
Orion Henry
e07211278f v0.0.14 2022-02-24 18:46:20 -05:00
Orion Henry
3c3f411329 update to new autotransaction api 2022-02-24 18:43:44 -05:00
Orion Henry
5aad691e31 Merge branch 'experiment' into marks 2022-02-24 18:10:19 -05:00
Orion Henry
872efc5756 v10 2022-02-24 17:41:55 -05:00
Orion Henry
e37395f975 make() defaults to text 2022-02-24 17:41:35 -05:00
Orion Henry
a84fa64554 change MAP,LIST,TEXT to be {},[],'' - allow recursion 2022-02-24 17:41:33 -05:00
Orion Henry
a37d4a6870 spans will now respect non-graphmem values 2022-02-24 16:41:01 -05:00
Blaine Cook
5eb5714c13 add failing test for marks handling in 3-way merge scenario 2022-02-24 16:24:17 -05:00
Blaine Cook
4f9b95b5b8 add test for merge behaviour of marks 2022-02-24 16:24:17 -05:00
Orion Henry
36b4f08d20 wasm to 0.0.7 2022-02-22 12:13:01 -05:00
Orion Henry
015e8ce465 choking on bad value function 2022-02-22 12:12:59 -05:00
Orion Henry
ea2f29d681 wasm to 0.0.6 2022-02-22 12:11:49 -05:00
Orion Henry
c8cd069e51 tweak files 2022-02-22 12:11:49 -05:00
Orion Henry
2ba2da95a8 attempt at new packaging 2022-02-22 12:11:49 -05:00
Orion Henry
561cad44e3 Revert "remove marks"
This reverts commit c8c695618b.
2022-02-22 12:11:49 -05:00
24 changed files with 989 additions and 731 deletions

View file

@ -1,5 +1,7 @@
/node_modules
/dev
/node
/web
/target
Cargo.lock
yarn.lock

View file

@ -3,7 +3,7 @@
name = "automerge-wasm"
description = "An js/wasm wrapper for the rust implementation of automerge-backend"
# repository = "https://github.com/automerge/automerge-rs"
version = "0.1.0"
version = "0.0.4"
authors = ["Alex Good <alex@memoryandthought.me>","Orion Henry <orion@inkandswitch.com>", "Martin Kleppmann"]
categories = ["wasm"]
readme = "README.md"
@ -40,10 +40,10 @@ version = "^0.2"
features = ["serde-serialize", "std"]
[package.metadata.wasm-pack.profile.release]
# wasm-opt = false
wasm-opt = true
[package.metadata.wasm-pack.profile.profiling]
wasm-opt = false
wasm-opt = true
# The `web-sys` crate allows you to interact with the various browser APIs,
# like the DOM.

View file

@ -2,692 +2,3 @@
This is a low level automerge library written in rust exporting a javascript API via WASM. This low level api is the underpinning to the `automerge-js` library that reimplements the Automerge API via these low level functions.
### Static Functions
### Methods
`doc.clone(actor?: string)` : Make a complete
`doc.free()` : deallocate WASM memory associated with a document
#[wasm_bindgen]
pub fn free(self) {}
#[wasm_bindgen(js_name = pendingOps)]
pub fn pending_ops(&self) -> JsValue {
(self.0.pending_ops() as u32).into()
}
pub fn commit(&mut self, message: Option<String>, time: Option<f64>) -> Array {
let heads = self.0.commit(message, time.map(|n| n as i64));
let heads: Array = heads
.iter()
.map(|h| JsValue::from_str(&hex::encode(&h.0)))
.collect();
heads
}
pub fn rollback(&mut self) -> f64 {
self.0.rollback() as f64
}
pub fn keys(&mut self, obj: String, heads: Option<Array>) -> Result<Array, JsValue> {
let obj = self.import(obj)?;
let result = if let Some(heads) = get_heads(heads) {
self.0.keys_at(&obj, &heads)
} else {
self.0.keys(&obj)
}
.iter()
.map(|s| JsValue::from_str(s))
.collect();
Ok(result)
}
pub fn text(&mut self, obj: String, heads: Option<Array>) -> Result<String, JsValue> {
let obj = self.import(obj)?;
if let Some(heads) = get_heads(heads) {
self.0.text_at(&obj, &heads)
} else {
self.0.text(&obj)
}
.map_err(to_js_err)
}
pub fn splice(
&mut self,
obj: String,
start: f64,
delete_count: f64,
text: JsValue,
) -> Result<Option<Array>, JsValue> {
let obj = self.import(obj)?;
let start = start as usize;
let delete_count = delete_count as usize;
let mut vals = vec![];
if let Some(t) = text.as_string() {
self.0
.splice_text(&obj, start, delete_count, &t)
.map_err(to_js_err)?;
Ok(None)
} else {
if let Ok(array) = text.dyn_into::<Array>() {
for i in array.iter() {
if let Ok(array) = i.clone().dyn_into::<Array>() {
let value = array.get(1);
let datatype = array.get(2);
let value = self.import_value(value, datatype.as_string())?;
vals.push(value);
} else {
let value = self.import_value(i, None)?;
vals.push(value);
}
}
}
let result = self
.0
.splice(&obj, start, delete_count, vals)
.map_err(to_js_err)?;
if result.is_empty() {
Ok(None)
} else {
let result: Array = result
.iter()
.map(|r| JsValue::from(r.to_string()))
.collect();
Ok(result.into())
}
}
}
pub fn push(
&mut self,
obj: String,
value: JsValue,
datatype: Option<String>,
) -> Result<Option<String>, JsValue> {
let obj = self.import(obj)?;
let value = self.import_value(value, datatype)?;
let index = self.0.length(&obj);
let opid = self.0.insert(&obj, index, value).map_err(to_js_err)?;
Ok(opid.map(|id| id.to_string()))
}
pub fn insert(
&mut self,
obj: String,
index: f64,
value: JsValue,
datatype: Option<String>,
) -> Result<Option<String>, JsValue> {
let obj = self.import(obj)?;
let index = index as f64;
let value = self.import_value(value, datatype)?;
let opid = self
.0
.insert(&obj, index as usize, value)
.map_err(to_js_err)?;
Ok(opid.map(|id| id.to_string()))
}
pub fn set(
&mut self,
obj: String,
prop: JsValue,
value: JsValue,
datatype: Option<String>,
) -> Result<Option<String>, JsValue> {
let obj = self.import(obj)?;
let prop = self.import_prop(prop)?;
let value = self.import_value(value, datatype)?;
let opid = self.0.set(&obj, prop, value).map_err(to_js_err)?;
Ok(opid.map(|id| id.to_string()))
}
pub fn make(
&mut self,
obj: String,
prop: JsValue,
value: JsValue,
) -> Result<String, JsValue> {
let obj = self.import(obj)?;
let prop = self.import_prop(prop)?;
let value = self.import_value(value, None)?;
if value.is_object() {
let opid = self.0.set(&obj, prop, value).map_err(to_js_err)?;
Ok(opid.unwrap().to_string())
} else {
Err("invalid object type".into())
}
}
pub fn inc(&mut self, obj: String, prop: JsValue, value: JsValue) -> Result<(), JsValue> {
let obj = self.import(obj)?;
let prop = self.import_prop(prop)?;
let value: f64 = value
.as_f64()
.ok_or("inc needs a numberic value")
.map_err(to_js_err)?;
self.0.inc(&obj, prop, value as i64).map_err(to_js_err)?;
Ok(())
}
pub fn value(
&mut self,
obj: String,
prop: JsValue,
heads: Option<Array>,
) -> Result<Array, JsValue> {
let obj = self.import(obj)?;
let result = Array::new();
let prop = to_prop(prop);
let heads = get_heads(heads);
if let Ok(prop) = prop {
let value = if let Some(h) = heads {
self.0.value_at(&obj, prop, &h)
} else {
self.0.value(&obj, prop)
}
.map_err(to_js_err)?;
match value {
Some((Value::Object(obj_type), obj_id)) => {
result.push(&obj_type.to_string().into());
result.push(&obj_id.to_string().into());
}
Some((Value::Scalar(value), _)) => {
result.push(&datatype(&value).into());
result.push(&ScalarValue(value).into());
}
None => {}
}
}
Ok(result)
}
pub fn values(
&mut self,
obj: String,
arg: JsValue,
heads: Option<Array>,
) -> Result<Array, JsValue> {
let obj = self.import(obj)?;
let result = Array::new();
let prop = to_prop(arg);
if let Ok(prop) = prop {
let values = if let Some(heads) = get_heads(heads) {
self.0.values_at(&obj, prop, &heads)
} else {
self.0.values(&obj, prop)
}
.map_err(to_js_err)?;
for value in values {
match value {
(Value::Object(obj_type), obj_id) => {
let sub = Array::new();
sub.push(&obj_type.to_string().into());
sub.push(&obj_id.to_string().into());
result.push(&sub.into());
}
(Value::Scalar(value), id) => {
let sub = Array::new();
sub.push(&datatype(&value).into());
sub.push(&ScalarValue(value).into());
sub.push(&id.to_string().into());
result.push(&sub.into());
}
}
}
}
Ok(result)
}
pub fn length(&mut self, obj: String, heads: Option<Array>) -> Result<f64, JsValue> {
let obj = self.import(obj)?;
if let Some(heads) = get_heads(heads) {
Ok(self.0.length_at(&obj, &heads) as f64)
} else {
Ok(self.0.length(&obj) as f64)
}
}
pub fn del(&mut self, obj: String, prop: JsValue) -> Result<(), JsValue> {
let obj = self.import(obj)?;
let prop = to_prop(prop)?;
self.0.del(&obj, prop).map_err(to_js_err)?;
Ok(())
}
pub fn mark(
&mut self,
obj: JsValue,
range: JsValue,
name: JsValue,
value: JsValue,
datatype: JsValue,
) -> Result<(), JsValue> {
let obj = self.import(obj)?;
let re = Regex::new(r"([\[\(])(\d+)\.\.(\d+)([\)\]])").unwrap();
let range = range.as_string().ok_or("range must be a string")?;
let cap = re.captures_iter(&range).next().ok_or("range must be in the form of (start..end] or [start..end) etc... () for sticky, [] for normal")?;
let start: usize = cap[2].parse().map_err(|_| to_js_err("invalid start"))?;
let end: usize = cap[3].parse().map_err(|_| to_js_err("invalid end"))?;
let start_sticky = &cap[1] == "(";
let end_sticky = &cap[4] == ")";
let name = name
.as_string()
.ok_or("invalid mark name")
.map_err(to_js_err)?;
let value = self.import_scalar(&value, datatype.as_string())?;
self.0
.mark(&obj, start, start_sticky, end, end_sticky, &name, value)
.map_err(to_js_err)?;
Ok(())
}
pub fn spans(&mut self, obj: JsValue) -> Result<JsValue, JsValue> {
let obj = self.import(obj)?;
let text = self.0.text(&obj).map_err(to_js_err)?;
let spans = self.0.spans(&obj).map_err(to_js_err)?;
let mut last_pos = 0;
let result = Array::new();
for s in spans {
let marks = Array::new();
for m in s.marks {
let mark = Array::new();
mark.push(&m.0.into());
mark.push(&datatype(&m.1).into());
mark.push(&ScalarValue(m.1).into());
marks.push(&mark.into());
}
let text_span = &text[last_pos..s.pos]; //.slice(last_pos, s.pos);
if text_span.len() > 0 {
result.push(&text_span.into());
}
result.push(&marks);
last_pos = s.pos;
//let obj = Object::new().into();
//js_set(&obj, "pos", s.pos as i32)?;
//js_set(&obj, "marks", marks)?;
//result.push(&obj.into());
}
let text_span = &text[last_pos..];
if text_span.len() > 0 {
result.push(&text_span.into());
}
Ok(result.into())
}
pub fn save(&mut self) -> Result<Uint8Array, JsValue> {
self.0
.save()
.map(|v| Uint8Array::from(v.as_slice()))
.map_err(to_js_err)
}
#[wasm_bindgen(js_name = saveIncremental)]
pub fn save_incremental(&mut self) -> Uint8Array {
let bytes = self.0.save_incremental();
Uint8Array::from(bytes.as_slice())
}
#[wasm_bindgen(js_name = loadIncremental)]
pub fn load_incremental(&mut self, data: Uint8Array) -> Result<f64, JsValue> {
let data = data.to_vec();
let len = self.0.load_incremental(&data).map_err(to_js_err)?;
Ok(len as f64)
}
#[wasm_bindgen(js_name = applyChanges)]
pub fn apply_changes(&mut self, changes: JsValue) -> Result<(), JsValue> {
let changes: Vec<_> = JS(changes).try_into()?;
self.0.apply_changes(&changes).map_err(to_js_err)?;
Ok(())
}
#[wasm_bindgen(js_name = getChanges)]
pub fn get_changes(&mut self, have_deps: JsValue) -> Result<Array, JsValue> {
let deps: Vec<_> = JS(have_deps).try_into()?;
let changes = self.0.get_changes(&deps);
let changes: Array = changes
.iter()
.map(|c| Uint8Array::from(c.raw_bytes()))
.collect();
Ok(changes)
}
#[wasm_bindgen(js_name = getChangesAdded)]
pub fn get_changes_added(&mut self, other: &Automerge) -> Result<Array, JsValue> {
let changes = self.0.get_changes_added(&other.0);
let changes: Array = changes
.iter()
.map(|c| Uint8Array::from(c.raw_bytes()))
.collect();
Ok(changes)
}
#[wasm_bindgen(js_name = getHeads)]
pub fn get_heads(&mut self) -> Array {
let heads = self.0.get_heads();
let heads: Array = heads
.iter()
.map(|h| JsValue::from_str(&hex::encode(&h.0)))
.collect();
heads
}
#[wasm_bindgen(js_name = getActorId)]
pub fn get_actor_id(&mut self) -> String {
let actor = self.0.get_actor();
actor.to_string()
}
#[wasm_bindgen(js_name = getLastLocalChange)]
pub fn get_last_local_change(&mut self) -> Result<Option<Uint8Array>, JsValue> {
if let Some(change) = self.0.get_last_local_change() {
Ok(Some(Uint8Array::from(change.raw_bytes())))
} else {
Ok(None)
}
}
pub fn dump(&self) {
self.0.dump()
}
#[wasm_bindgen(js_name = getMissingDeps)]
pub fn get_missing_deps(&mut self, heads: Option<Array>) -> Result<Array, JsValue> {
let heads = get_heads(heads).unwrap_or_default();
let deps = self.0.get_missing_deps(&heads);
let deps: Array = deps
.iter()
.map(|h| JsValue::from_str(&hex::encode(&h.0)))
.collect();
Ok(deps)
}
#[wasm_bindgen(js_name = receiveSyncMessage)]
pub fn receive_sync_message(
&mut self,
state: &mut SyncState,
message: Uint8Array,
) -> Result<(), JsValue> {
let message = message.to_vec();
let message = am::SyncMessage::decode(message.as_slice()).map_err(to_js_err)?;
self.0
.receive_sync_message(&mut state.0, message)
.map_err(to_js_err)?;
Ok(())
}
#[wasm_bindgen(js_name = generateSyncMessage)]
pub fn generate_sync_message(&mut self, state: &mut SyncState) -> Result<JsValue, JsValue> {
if let Some(message) = self.0.generate_sync_message(&mut state.0) {
Ok(Uint8Array::from(message.encode().map_err(to_js_err)?.as_slice()).into())
} else {
Ok(JsValue::null())
}
}
#[wasm_bindgen(js_name = toJS)]
pub fn to_js(&self) -> JsValue {
map_to_js(&self.0, &ROOT)
}
fn import(&self, id: String) -> Result<ObjId, JsValue> {
self.0.import(&id).map_err(to_js_err)
}
fn import_prop(&mut self, prop: JsValue) -> Result<Prop, JsValue> {
if let Some(s) = prop.as_string() {
Ok(s.into())
} else if let Some(n) = prop.as_f64() {
Ok((n as usize).into())
} else {
Err(format!("invalid prop {:?}", prop).into())
}
}
fn import_scalar(
&mut self,
value: &JsValue,
datatype: Option<String>,
) -> Result<am::ScalarValue, JsValue> {
match datatype.as_deref() {
Some("boolean") => value
.as_bool()
.ok_or_else(|| "value must be a bool".into())
.map(am::ScalarValue::Boolean),
Some("int") => value
.as_f64()
.ok_or_else(|| "value must be a number".into())
.map(|v| am::ScalarValue::Int(v as i64)),
Some("uint") => value
.as_f64()
.ok_or_else(|| "value must be a number".into())
.map(|v| am::ScalarValue::Uint(v as u64)),
Some("f64") => value
.as_f64()
.ok_or_else(|| "value must be a number".into())
.map(am::ScalarValue::F64),
Some("bytes") => Ok(am::ScalarValue::Bytes(
value.clone().dyn_into::<Uint8Array>().unwrap().to_vec(),
)),
Some("counter") => value
.as_f64()
.ok_or_else(|| "value must be a number".into())
.map(|v| am::ScalarValue::counter(v as i64)),
Some("timestamp") => value
.as_f64()
.ok_or_else(|| "value must be a number".into())
.map(|v| am::ScalarValue::Timestamp(v as i64)),
/*
Some("bytes") => unimplemented!(),
Some("cursor") => unimplemented!(),
*/
Some("null") => Ok(am::ScalarValue::Null),
Some(_) => Err(format!("unknown datatype {:?}", datatype).into()),
None => {
if value.is_null() {
Ok(am::ScalarValue::Null)
} else if let Some(b) = value.as_bool() {
Ok(am::ScalarValue::Boolean(b))
} else if let Some(s) = value.as_string() {
// FIXME - we need to detect str vs int vs float vs bool here :/
Ok(am::ScalarValue::Str(s.into()))
} else if let Some(n) = value.as_f64() {
if (n.round() - n).abs() < f64::EPSILON {
Ok(am::ScalarValue::Int(n as i64))
} else {
Ok(am::ScalarValue::F64(n))
}
// } else if let Some(o) = to_objtype(&value) {
// Ok(o.into())
} else if let Ok(d) = value.clone().dyn_into::<js_sys::Date>() {
Ok(am::ScalarValue::Timestamp(d.get_time() as i64))
} else if let Ok(o) = &value.clone().dyn_into::<Uint8Array>() {
Ok(am::ScalarValue::Bytes(o.to_vec()))
} else {
Err("value is invalid".into())
}
}
}
}
fn import_value(&mut self, value: JsValue, datatype: Option<String>) -> Result<Value, JsValue> {
match self.import_scalar(&value, datatype) {
Ok(val) => Ok(val.into()),
Err(err) => {
if let Some(o) = to_objtype(&value) {
Ok(o.into())
} else {
Err(err)
}
}
}
/*
match datatype.as_deref() {
Some("boolean") => value
.as_bool()
.ok_or_else(|| "value must be a bool".into())
.map(|v| am::ScalarValue::Boolean(v).into()),
Some("int") => value
.as_f64()
.ok_or_else(|| "value must be a number".into())
.map(|v| am::ScalarValue::Int(v as i64).into()),
Some("uint") => value
.as_f64()
.ok_or_else(|| "value must be a number".into())
.map(|v| am::ScalarValue::Uint(v as u64).into()),
Some("f64") => value
.as_f64()
.ok_or_else(|| "value must be a number".into())
.map(|n| am::ScalarValue::F64(n).into()),
Some("bytes") => {
Ok(am::ScalarValue::Bytes(value.dyn_into::<Uint8Array>().unwrap().to_vec()).into())
}
Some("counter") => value
.as_f64()
.ok_or_else(|| "value must be a number".into())
.map(|v| am::ScalarValue::counter(v as i64).into()),
Some("timestamp") => value
.as_f64()
.ok_or_else(|| "value must be a number".into())
.map(|v| am::ScalarValue::Timestamp(v as i64).into()),
Some("null") => Ok(am::ScalarValue::Null.into()),
Some(_) => Err(format!("unknown datatype {:?}", datatype).into()),
None => {
if value.is_null() {
Ok(am::ScalarValue::Null.into())
} else if let Some(b) = value.as_bool() {
Ok(am::ScalarValue::Boolean(b).into())
} else if let Some(s) = value.as_string() {
// FIXME - we need to detect str vs int vs float vs bool here :/
Ok(am::ScalarValue::Str(s.into()).into())
} else if let Some(n) = value.as_f64() {
if (n.round() - n).abs() < f64::EPSILON {
Ok(am::ScalarValue::Int(n as i64).into())
} else {
Ok(am::ScalarValue::F64(n).into())
}
} else if let Some(o) = to_objtype(&value) {
Ok(o.into())
} else if let Ok(d) = value.clone().dyn_into::<js_sys::Date>() {
Ok(am::ScalarValue::Timestamp(d.get_time() as i64).into())
} else if let Ok(o) = &value.dyn_into::<Uint8Array>() {
Ok(am::ScalarValue::Bytes(o.to_vec()).into())
} else {
Err("value is invalid".into())
}
}
}
*/
}
}
#[wasm_bindgen(js_name = create)]
pub fn init(actor: Option<String>) -> Result<Automerge, JsValue> {
console_error_panic_hook::set_once();
Automerge::new(actor)
}
#[wasm_bindgen(js_name = loadDoc)]
pub fn load(data: Uint8Array, actor: Option<String>) -> Result<Automerge, JsValue> {
let data = data.to_vec();
let mut automerge = am::Automerge::load(&data).map_err(to_js_err)?;
if let Some(s) = actor {
let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec());
automerge.set_actor(actor)
}
Ok(Automerge(automerge))
}
#[wasm_bindgen(js_name = encodeChange)]
pub fn encode_change(change: JsValue) -> Result<Uint8Array, JsValue> {
let change: am::ExpandedChange = change.into_serde().map_err(to_js_err)?;
let change: Change = change.into();
Ok(Uint8Array::from(change.raw_bytes()))
}
#[wasm_bindgen(js_name = decodeChange)]
pub fn decode_change(change: Uint8Array) -> Result<JsValue, JsValue> {
let change = Change::from_bytes(change.to_vec()).map_err(to_js_err)?;
let change: am::ExpandedChange = change.decode();
JsValue::from_serde(&change).map_err(to_js_err)
}
#[wasm_bindgen(js_name = initSyncState)]
pub fn init_sync_state() -> SyncState {
SyncState(am::SyncState::new())
}
// this is needed to be compatible with the automerge-js api
#[wasm_bindgen(js_name = importSyncState)]
pub fn import_sync_state(state: JsValue) -> Result<SyncState, JsValue> {
Ok(SyncState(JS(state).try_into()?))
}
// this is needed to be compatible with the automerge-js api
#[wasm_bindgen(js_name = exportSyncState)]
pub fn export_sync_state(state: SyncState) -> JsValue {
JS::from(state.0).into()
}
#[wasm_bindgen(js_name = encodeSyncMessage)]
pub fn encode_sync_message(message: JsValue) -> Result<Uint8Array, JsValue> {
let heads = js_get(&message, "heads")?.try_into()?;
let need = js_get(&message, "need")?.try_into()?;
let changes = js_get(&message, "changes")?.try_into()?;
let have = js_get(&message, "have")?.try_into()?;
Ok(Uint8Array::from(
am::SyncMessage {
heads,
need,
have,
changes,
}
.encode()
.unwrap()
.as_slice(),
))
}
#[wasm_bindgen(js_name = decodeSyncMessage)]
pub fn decode_sync_message(msg: Uint8Array) -> Result<JsValue, JsValue> {
let data = msg.to_vec();
let msg = am::SyncMessage::decode(&data).map_err(to_js_err)?;
let heads = AR::from(msg.heads.as_slice());
let need = AR::from(msg.need.as_slice());
let changes = AR::from(msg.changes.as_slice());
let have = AR::from(msg.have.as_slice());
let obj = Object::new().into();
js_set(&obj, "heads", heads)?;
js_set(&obj, "need", need)?;
js_set(&obj, "have", have)?;
js_set(&obj, "changes", changes)?;
Ok(obj)
}
#[wasm_bindgen(js_name = encodeSyncState)]
pub fn encode_sync_state(state: SyncState) -> Result<Uint8Array, JsValue> {
let state = state.0;
Ok(Uint8Array::from(
state.encode().map_err(to_js_err)?.as_slice(),
))
}
#[wasm_bindgen(js_name = decodeSyncState)]
pub fn decode_sync_state(data: Uint8Array) -> Result<SyncState, JsValue> {
SyncState::decode(data)
}
#[wasm_bindgen(js_name = MAP)]
pub struct Map {}
#[wasm_bindgen(js_name = LIST)]
pub struct List {}
#[wasm_bindgen(js_name = TEXT)]
pub struct Text {}
#[wasm_bindgen(js_name = TABLE)]
pub struct Table {}

View file

@ -98,6 +98,11 @@ export class Automerge {
text(obj: ObjID, heads?: Heads): string;
length(obj: ObjID, heads?: Heads): number;
// experimental spans api - unstable!
mark(obj: ObjID, name: string, range: string, value: Value, datatype?: Datatype): void;
spans(obj: ObjID): any;
raw_spans(obj: ObjID): any;
// transactions
commit(message?: string, time?: number): Heads;
merge(other: Automerge): Heads;

View file

@ -4,24 +4,27 @@
"Alex Good <alex@memoryandthought.me>",
"Martin Kleppmann"
],
"name": "automerge-wasm",
"name": "automerge-wasm-pack",
"description": "wasm-bindgen bindings to the automerge rust implementation",
"version": "0.0.1",
"version": "0.0.14",
"license": "MIT",
"files": [
"README.md",
"LICENSE",
"package.json",
"automerge_wasm_bg.wasm",
"automerge_wasm.js"
"index.d.ts",
"node/index.js",
"node/index_bg.wasm",
"web/index.js",
"web/index_bg.wasm"
],
"module": "./pkg/index.js",
"main": "./dev/index.js",
"types": "index.d.ts",
"module": "./web/index.js",
"main": "./node/index.js",
"scripts": {
"build": "rimraf ./dev && wasm-pack build --target nodejs --dev --out-name index -d dev && cp index.d.ts dev",
"release": "rimraf ./dev && wasm-pack build --target nodejs --release --out-name index -d dev && cp index.d.ts dev",
"pkg": "rimraf ./pkg && wasm-pack build --target web --release --out-name index -d pkg && cp index.d.ts pkg && cd pkg && yarn pack && mv automerge-wasm*tgz ..",
"prof": "rimraf ./dev && wasm-pack build --target nodejs --profiling --out-name index -d dev",
"build": "rimraf ./node && wasm-pack build --target nodejs --dev --out-name index -d node && cp index.d.ts node",
"release-w": "rimraf ./web && wasm-pack build --target web --dev --out-name index -d web && cp index.d.ts web",
"release-n": "rimraf ./node && wasm-pack build --target nodejs --dev --out-name index -d node && cp index.d.ts node",
"release": "yarn release-w && yarn release-n",
"test": "yarn build && ts-mocha -p tsconfig.json --type-check --bail --full-trace test/*.ts"
},
"dependencies": {},

View file

@ -4,6 +4,7 @@ use am::transaction::Transactable;
use automerge as am;
use automerge::{Change, ObjId, Prop, Value, ROOT};
use js_sys::{Array, Object, Uint8Array};
use regex::Regex;
use std::convert::TryInto;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
@ -332,6 +333,86 @@ impl Automerge {
Ok(())
}
pub fn mark(
&mut self,
obj: JsValue,
range: JsValue,
name: JsValue,
value: JsValue,
datatype: JsValue,
) -> Result<(), JsValue> {
let obj = self.import(obj)?;
let re = Regex::new(r"([\[\(])(\d+)\.\.(\d+)([\)\]])").unwrap();
let range = range.as_string().ok_or("range must be a string")?;
let cap = re.captures_iter(&range).next().ok_or("range must be in the form of (start..end] or [start..end) etc... () for sticky, [] for normal")?;
let start: usize = cap[2].parse().map_err(|_| to_js_err("invalid start"))?;
let end: usize = cap[3].parse().map_err(|_| to_js_err("invalid end"))?;
let start_sticky = &cap[1] == "(";
let end_sticky = &cap[4] == ")";
let name = name
.as_string()
.ok_or("invalid mark name")
.map_err(to_js_err)?;
let value = self
.import_scalar(&value, &datatype.as_string())
.ok_or_else(|| to_js_err("invalid value"))?;
self.0
.mark(&obj, start, start_sticky, end, end_sticky, &name, value)
.map_err(to_js_err)?;
Ok(())
}
pub fn spans(&mut self, obj: JsValue) -> Result<JsValue, JsValue> {
let obj = self.import(obj)?;
let text = self.0.list(&obj).map_err(to_js_err)?;
let spans = self.0.spans(&obj).map_err(to_js_err)?;
let mut last_pos = 0;
let result = Array::new();
for s in spans {
let marks = Array::new();
for m in s.marks {
let mark = Array::new();
mark.push(&m.0.into());
mark.push(&datatype(&m.1).into());
mark.push(&ScalarValue(m.1).into());
marks.push(&mark.into());
}
let text_span = &text[last_pos..s.pos]; //.slice(last_pos, s.pos);
if !text_span.is_empty() {
let t: String = text_span
.iter()
.filter_map(|(v, _)| v.as_string())
.collect();
result.push(&t.into());
}
result.push(&marks);
last_pos = s.pos;
//let obj = Object::new().into();
//js_set(&obj, "pos", s.pos as i32)?;
//js_set(&obj, "marks", marks)?;
//result.push(&obj.into());
}
let text_span = &text[last_pos..];
if !text_span.is_empty() {
let t: String = text_span
.iter()
.filter_map(|(v, _)| v.as_string())
.collect();
result.push(&t.into());
}
Ok(result.into())
}
pub fn raw_spans(&mut self, obj: JsValue) -> Result<Array, JsValue> {
let obj = self.import(obj)?;
let spans = self.0.raw_spans(&obj).map_err(to_js_err)?;
let result = Array::new();
for s in spans {
result.push(&JsValue::from_serde(&s).map_err(to_js_err)?);
}
Ok(result)
}
pub fn save(&mut self) -> Result<Uint8Array, JsValue> {
self.0
.save()

View file

@ -0,0 +1,195 @@
import { describe, it } from 'mocha';
//@ts-ignore
import assert from 'assert'
//@ts-ignore
import { create, loadDoc, Automerge, encodeChange, decodeChange } from '..'
describe('Automerge', () => {
describe('marks', () => {
it('should handle marks [..]', () => {
let doc = create()
let list = doc.make("_root", "list", "")
if (!list) throw new Error('should not be undefined')
doc.splice(list, 0, 0, "aaabbbccc")
doc.mark(list, "[3..6]", "bold" , true)
let spans = doc.spans(list);
assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'ccc' ]);
doc.insert(list, 6, "A")
doc.insert(list, 3, "A")
spans = doc.spans(list);
assert.deepStrictEqual(spans, [ 'aaaA', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'Accc' ]);
})
it('should handle marks [..] at the beginning of a string', () => {
let doc = create()
let list = doc.make("_root", "list", "")
if (!list) throw new Error('should not be undefined')
doc.splice(list, 0, 0, "aaabbbccc")
doc.mark(list, "[0..3]", "bold", true)
let spans = doc.spans(list);
assert.deepStrictEqual(spans, [ [ [ 'bold', 'boolean', true ] ], 'aaa', [], 'bbbccc' ]);
let doc2 = doc.fork()
doc2.insert(list, 0, "A")
doc2.insert(list, 4, "B")
doc.merge(doc2)
spans = doc.spans(list);
assert.deepStrictEqual(spans, [ 'A', [ [ 'bold', 'boolean', true ] ], 'aaa', [], 'Bbbbccc' ]);
})
it('should handle marks [..] with splice', () => {
let doc = create()
let list = doc.make("_root", "list", "")
if (!list) throw new Error('should not be undefined')
doc.splice(list, 0, 0, "aaabbbccc")
doc.mark(list, "[0..3]", "bold", true)
let spans = doc.spans(list);
assert.deepStrictEqual(spans, [ [ [ 'bold', 'boolean', true ] ], 'aaa', [], 'bbbccc' ]);
let doc2 = doc.fork()
doc2.splice(list, 0, 2, "AAA")
doc2.splice(list, 4, 0, "BBB")
doc.merge(doc2)
spans = doc.spans(list);
assert.deepStrictEqual(spans, [ 'AAA', [ [ 'bold', 'boolean', true ] ], 'a', [], 'BBBbbbccc' ]);
})
it('should handle marks across multiple forks', () => {
let doc = create()
let list = doc.make("_root", "list", "")
if (!list) throw new Error('should not be undefined')
doc.splice(list, 0, 0, "aaabbbccc")
doc.mark(list, "[0..3]", "bold", true)
let spans = doc.spans(list);
assert.deepStrictEqual(spans, [ [ [ 'bold', 'boolean', true ] ], 'aaa', [], 'bbbccc' ]);
let doc2 = doc.fork()
doc2.splice(list, 1, 1, "Z") // replace 'aaa' with 'aZa' inside mark.
let doc3 = doc.fork()
doc3.insert(list, 0, "AAA") // should not be included in mark.
doc.merge(doc2)
doc.merge(doc3)
spans = doc.spans(list);
assert.deepStrictEqual(spans, [ 'AAA', [ [ 'bold', 'boolean', true ] ], 'aZa', [], 'bbbccc' ]);
})
it('should handle marks with deleted ends [..]', () => {
let doc = create()
let list = doc.make("_root", "list", "")
if (!list) throw new Error('should not be undefined')
doc.splice(list, 0, 0, "aaabbbccc")
doc.mark(list, "[3..6]", "bold" , true)
let spans = doc.spans(list);
assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'ccc' ]);
doc.del(list,5);
doc.del(list,5);
doc.del(list,2);
doc.del(list,2);
spans = doc.spans(list);
assert.deepStrictEqual(spans, [ 'aa', [ [ 'bold', 'boolean', true ] ], 'b', [], 'cc' ])
doc.insert(list, 3, "A")
doc.insert(list, 2, "A")
spans = doc.spans(list);
assert.deepStrictEqual(spans, [ 'aaA', [ [ 'bold', 'boolean', true ] ], 'b', [], 'Acc' ])
})
it('should handle sticky marks (..)', () => {
let doc = create()
let list = doc.make("_root", "list", "")
if (!list) throw new Error('should not be undefined')
doc.splice(list, 0, 0, "aaabbbccc")
doc.mark(list, "(3..6)", "bold" , true)
let spans = doc.spans(list);
assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'ccc' ]);
doc.insert(list, 6, "A")
doc.insert(list, 3, "A")
spans = doc.spans(list);
assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'AbbbA', [], 'ccc' ]);
})
it('should handle sticky marks with deleted ends (..)', () => {
let doc = create()
let list = doc.make("_root", "list", "")
if (!list) throw new Error('should not be undefined')
doc.splice(list, 0, 0, "aaabbbccc")
doc.mark(list, "(3..6)", "bold" , true)
let spans = doc.spans(list);
assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'ccc' ]);
doc.del(list,5);
doc.del(list,5);
doc.del(list,2);
doc.del(list,2);
spans = doc.spans(list);
assert.deepStrictEqual(spans, [ 'aa', [ [ 'bold', 'boolean', true ] ], 'b', [], 'cc' ])
doc.insert(list, 3, "A")
doc.insert(list, 2, "A")
spans = doc.spans(list);
assert.deepStrictEqual(spans, [ 'aa', [ [ 'bold', 'boolean', true ] ], 'AbA', [], 'cc' ])
// make sure save/load can handle marks
let doc2 = loadDoc(doc.save())
spans = doc2.spans(list);
assert.deepStrictEqual(spans, [ 'aa', [ [ 'bold', 'boolean', true ] ], 'AbA', [], 'cc' ])
assert.deepStrictEqual(doc.getHeads(), doc2.getHeads())
assert.deepStrictEqual(doc.save(), doc2.save())
})
it('should handle overlapping marks', () => {
let doc : Automerge = create("aabbcc")
let list = doc.make("_root", "list", "")
if (!list) throw new Error('should not be undefined')
doc.splice(list, 0, 0, "the quick fox jumps over the lazy dog")
doc.mark(list, "[0..37]", "bold" , true)
doc.mark(list, "[4..19]", "itallic" , true)
doc.mark(list, "[10..13]", "comment" , "foxes are my favorite animal!")
doc.commit("marks",999);
let spans = doc.spans(list);
assert.deepStrictEqual(spans,
[
[ [ 'bold', 'boolean', true ] ],
'the ',
[ [ 'bold', 'boolean', true ], [ 'itallic', 'boolean', true ] ],
'quick ',
[
[ 'bold', 'boolean', true ],
[ 'comment', 'str', 'foxes are my favorite animal!' ],
[ 'itallic', 'boolean', true ]
],
'fox',
[ [ 'bold', 'boolean', true ], [ 'itallic', 'boolean', true ] ],
' jumps',
[ [ 'bold', 'boolean', true ] ],
' over the lazy dog',
[],
]
)
let text = doc.text(list);
assert.deepStrictEqual(text, "the quick fox jumps over the lazy dog");
let raw_spans = doc.raw_spans(list);
assert.deepStrictEqual(raw_spans,
[
{ id: "39@aabbcc", time: 999, start: 0, end: 37, type: 'bold', value: true },
{ id: "41@aabbcc", time: 999, start: 4, end: 19, type: 'itallic', value: true },
{ id: "43@aabbcc", time: 999, start: 10, end: 13, type: 'comment', value: 'foxes are my favorite animal!' }
]);
// mark sure encode decode can handle marks
let all = doc.getChanges([])
let decoded = all.map((c) => decodeChange(c))
let encoded = decoded.map((c) => encodeChange(c))
let doc2 = create();
doc2.applyChanges(encoded)
assert.deepStrictEqual(doc.spans(list) , doc2.spans(list))
assert.deepStrictEqual(doc.save(), doc2.save())
})
})
})

View file

@ -3,9 +3,8 @@ import { describe, it } from 'mocha';
import assert from 'assert'
//@ts-ignore
import { BloomFilter } from './helpers/sync'
import { create, loadDoc, SyncState, Automerge, encodeChange, decodeChange, initSyncState, decodeSyncMessage, decodeSyncState, encodeSyncState, encodeSyncMessage } from '../dev/index'
import { DecodedSyncMessage } from '../index';
import { Hash } from '../dev/index';
import { create, loadDoc, SyncState, Automerge, encodeChange, decodeChange, initSyncState, decodeSyncMessage, decodeSyncState, encodeSyncState, encodeSyncMessage } from '..'
import { DecodedSyncMessage, Hash } from '..'
function sync(a: Automerge, b: Automerge, aSyncState = initSyncState(), bSyncState = initSyncState()) {
const MAX_ITER = 10

View file

@ -2,8 +2,8 @@ use crate::exid::ExId;
use crate::transaction::{CommitOptions, Transactable};
use crate::types::Patch;
use crate::{
change::export_change, transaction::TransactionInner, ActorId, Automerge, AutomergeError,
Change, ChangeHash, Prop, Value,
change::export_change, query, transaction::TransactionInner, ActorId, Automerge,
AutomergeError, Change, ChangeHash, Prop, ScalarValue, Value,
};
use crate::{SyncMessage, SyncState};
@ -347,6 +347,31 @@ impl Transactable for AutoCommit {
tx.insert(&mut self.doc, obj, index, value)
}
#[allow(clippy::too_many_arguments)]
fn mark(
&mut self,
obj: &ExId,
start: usize,
expand_start: bool,
end: usize,
expand_end: bool,
mark: &str,
value: ScalarValue,
) -> Result<(), AutomergeError> {
self.ensure_transaction_open();
let tx = self.transaction.as_mut().unwrap();
tx.mark(
&mut self.doc,
obj,
start,
expand_start,
end,
expand_end,
mark,
value,
)
}
fn inc<P: Into<Prop>>(
&mut self,
obj: &ExId,
@ -386,6 +411,26 @@ impl Transactable for AutoCommit {
self.doc.text_at(obj, heads)
}
fn list(&self, obj: &ExId) -> Result<Vec<(Value, ExId)>, AutomergeError> {
self.doc.list(obj)
}
fn list_at(
&self,
obj: &ExId,
heads: &[ChangeHash],
) -> Result<Vec<(Value, ExId)>, AutomergeError> {
self.doc.list_at(obj, heads)
}
fn spans(&self, obj: &ExId) -> Result<Vec<query::Span>, AutomergeError> {
self.doc.spans(obj)
}
fn raw_spans(&self, obj: &ExId) -> Result<Vec<query::SpanInfo>, AutomergeError> {
self.doc.raw_spans(obj)
}
// TODO - I need to return these OpId's here **only** to get
// the legacy conflicts format of { [opid]: value }
// Something better?

View file

@ -13,7 +13,6 @@ use crate::types::{
};
use crate::{legacy, query, types, ObjType};
use crate::{AutomergeError, Change, Prop};
use serde::Serialize;
/// An automerge document.
#[derive(Debug, Clone)]
@ -284,6 +283,114 @@ impl Automerge {
Ok(buffer)
}
pub fn list(&self, obj: &ExId) -> Result<Vec<(Value, ExId)>, AutomergeError> {
let obj = self.exid_to_obj(obj)?;
let query = self.ops.search(obj, query::ListVals::new());
Ok(query
.ops
.iter()
.map(|o| (o.value(), self.id_to_exid(o.id)))
.collect())
}
pub fn list_at(
&self,
obj: &ExId,
heads: &[ChangeHash],
) -> Result<Vec<(Value, ExId)>, AutomergeError> {
let obj = self.exid_to_obj(obj)?;
let clock = self.clock_at(heads);
let query = self.ops.search(obj, query::ListValsAt::new(clock));
Ok(query
.ops
.iter()
.map(|o| (o.value(), self.id_to_exid(o.id)))
.collect())
}
pub fn spans(&self, obj: &ExId) -> Result<Vec<query::Span>, AutomergeError> {
let obj = self.exid_to_obj(obj)?;
let mut query = self.ops.search(obj, query::Spans::new());
query.check_marks();
Ok(query.spans)
}
pub fn blame(
&self,
obj: &ExId,
base: &[ChangeHash],
points: &[Vec<ChangeHash>],
) -> Result<Vec<bool>, AutomergeError> {
let obj = self.exid_to_obj(obj)?;
let base = self.clock_at(base);
let points: Vec<Clock> = points.iter().map(|p| self.clock_at(p)).collect();
let points: Vec<Clock> = points
.iter()
.enumerate()
.map(|(j, _)| {
points
.iter()
.enumerate()
.filter(|(i, _)| *i != j)
.fold(base.clone(), |acc, (_, c)| acc.union(c))
})
.collect();
let query = self.ops.search(obj, query::Blame::new(points));
//Ok(query.points)
unimplemented!()
}
pub fn raw_spans(&self, obj: &ExId) -> Result<Vec<query::SpanInfo>, AutomergeError> {
let obj = self.exid_to_obj(obj)?;
let query = self.ops.search(obj, query::RawSpans::new());
let result = query
.spans
.into_iter()
.map(|s| query::SpanInfo {
id: self.id_to_exid(s.id),
time: self.history[s.change].time,
start: s.start,
end: s.end,
span_type: s.name,
value: s.value,
})
.collect();
Ok(result)
}
/*
#[allow(clippy::too_many_arguments)]
pub fn mark(
&mut self,
obj: &ExId,
start: usize,
expand_start: bool,
end: usize,
expand_end: bool,
mark: &str,
value: ScalarValue,
) -> Result<(), AutomergeError> {
let obj = self.exid_to_obj(obj)?;
self.do_insert(obj, start, OpType::mark(mark.into(), expand_start, value))?;
self.do_insert(obj, end, OpType::MarkEnd(expand_end))?;
Ok(())
}
pub fn unmark(
&self,
_obj: &ExId,
_start: usize,
_end: usize,
_inclusive: bool,
_mark: &str,
) -> Result<String, AutomergeError> {
unimplemented!()
}
*/
// TODO - I need to return these OpId's here **only** to get
// the legacy conflicts format of { [opid]: value }
// Something better?
@ -823,6 +930,8 @@ impl Automerge {
OpType::Set(value) => format!("{}", value),
OpType::Make(obj) => format!("make({})", obj),
OpType::Inc(obj) => format!("inc({})", obj),
OpType::MarkBegin(m) => format!("mark({}={})", m.name, m.value),
OpType::MarkEnd(_) => "/mark".into(),
OpType::Del => format!("del{}", 0),
};
let pred: Vec<_> = i.pred.iter().map(|id| self.to_string(*id)).collect();
@ -851,17 +960,6 @@ impl Default for Automerge {
}
}
#[derive(Serialize, Debug, Clone, PartialEq)]
pub struct SpanInfo {
pub id: ExId,
pub time: i64,
pub start: usize,
pub end: usize,
#[serde(rename = "type")]
pub span_type: String,
pub value: ScalarValue,
}
#[cfg(test)]
mod tests {
use super::*;
@ -1124,8 +1222,6 @@ mod tests {
assert!(doc.value_at(&list, 0, &heads2)?.unwrap().0 == Value::int(10));
assert!(doc.length_at(&list, &heads3) == 2);
doc.dump();
//log!("{:?}", doc.value_at(&list, 0, &heads3)?.unwrap().0);
assert!(doc.value_at(&list, 0, &heads3)?.unwrap().0 == Value::int(30));
assert!(doc.value_at(&list, 1, &heads3)?.unwrap().0 == Value::int(20));

View file

@ -18,6 +18,13 @@ impl Clock {
.or_insert(n);
}
pub fn union(mut self, other: &Clock) -> Clock {
for (key, val) in &other.0 {
self.include(*key, *val)
}
self
}
pub fn covers(&self, id: &OpId) -> bool {
if let Some(val) = self.0.get(&id.1) {
val >= &id.0

View file

@ -134,6 +134,15 @@ impl<'a> Iterator for OperationIterator<'a> {
Action::MakeTable => OpType::Make(ObjType::Table),
Action::Del => OpType::Del,
Action::Inc => OpType::Inc(value.to_i64()?),
Action::MarkBegin => {
// mark has 3 things in the val column
let name = value.to_string()?;
let expand = self.value.next()?.to_bool()?;
let value = self.value.next()?;
OpType::mark(name, expand, value)
}
Action::MarkEnd => OpType::MarkEnd(value.to_bool()?),
Action::Unused => panic!("invalid action"),
};
Some(amp::Op {
action,
@ -175,6 +184,15 @@ impl<'a> Iterator for DocOpIterator<'a> {
Action::MakeTable => OpType::Make(ObjType::Table),
Action::Del => OpType::Del,
Action::Inc => OpType::Inc(value.to_i64()?),
Action::MarkBegin => {
// mark has 3 things in the val column
let name = value.to_string()?;
let expand = self.value.next()?.to_bool()?;
let value = self.value.next()?;
OpType::mark(name, expand, value)
}
Action::MarkEnd => OpType::MarkEnd(value.to_bool()?),
Action::Unused => panic!("invalid action"),
};
Some(DocOp {
actor,
@ -1064,6 +1082,16 @@ impl DocOpEncoder {
self.val.append_null();
Action::Del
}
amp::OpType::MarkBegin(m) => {
self.val.append_value(&m.name.clone().into(), actors);
self.val.append_value(&m.expand.into(), actors);
self.val.append_value(&m.value.clone(), actors);
Action::MarkBegin
}
amp::OpType::MarkEnd(s) => {
self.val.append_value(&(*s).into(), actors);
Action::MarkEnd
}
amp::OpType::Make(kind) => {
self.val.append_null();
match kind {
@ -1170,6 +1198,16 @@ impl ColumnEncoder {
self.val.append_null();
Action::Del
}
OpType::MarkBegin(m) => {
self.val.append_value2(&m.name.clone().into(), actors);
self.val.append_value2(&m.expand.into(), actors);
self.val.append_value2(&m.value.clone(), actors);
Action::MarkBegin
}
OpType::MarkEnd(s) => {
self.val.append_value2(&(*s).into(), actors);
Action::MarkEnd
}
OpType::Make(kind) => {
self.val.append_null();
match kind {
@ -1275,8 +1313,11 @@ pub(crate) enum Action {
MakeText,
Inc,
MakeTable,
MarkBegin,
Unused, // final bit is used to mask `Make` actions
MarkEnd,
}
const ACTIONS: [Action; 7] = [
const ACTIONS: [Action; 10] = [
Action::MakeMap,
Action::Set,
Action::MakeList,
@ -1284,6 +1325,9 @@ const ACTIONS: [Action; 7] = [
Action::MakeText,
Action::Inc,
Action::MakeTable,
Action::MarkBegin,
Action::Unused,
Action::MarkEnd,
];
impl Decodable for Action {

View file

@ -49,6 +49,12 @@ impl Serialize for Op {
match &self.action {
OpType::Inc(n) => op.serialize_field("value", &n)?,
OpType::Set(value) => op.serialize_field("value", &value)?,
OpType::MarkBegin(m) => {
op.serialize_field("name", &m.name)?;
op.serialize_field("expand", &m.expand)?;
op.serialize_field("value", &m.value)?;
}
OpType::MarkEnd(s) => op.serialize_field("expand", &s)?,
_ => {}
}
op.serialize_field("pred", &self.pred)?;
@ -70,6 +76,8 @@ pub(crate) enum RawOpType {
Del,
Inc,
Set,
MarkBegin,
MarkEnd,
}
impl Serialize for RawOpType {
@ -85,6 +93,8 @@ impl Serialize for RawOpType {
RawOpType::Del => "del",
RawOpType::Inc => "inc",
RawOpType::Set => "set",
RawOpType::MarkBegin => "mark_begin",
RawOpType::MarkEnd => "mark_end",
};
serializer.serialize_str(s)
}
@ -116,6 +126,8 @@ impl<'de> Deserialize<'de> for RawOpType {
"del" => Ok(RawOpType::Del),
"inc" => Ok(RawOpType::Inc),
"set" => Ok(RawOpType::Set),
"mark_begin" => Ok(RawOpType::MarkBegin),
"mark_end" => Ok(RawOpType::MarkEnd),
other => Err(Error::unknown_variant(other, VARIANTS)),
}
}
@ -188,6 +200,30 @@ impl<'de> Deserialize<'de> for Op {
RawOpType::MakeList => OpType::Make(ObjType::List),
RawOpType::MakeText => OpType::Make(ObjType::Text),
RawOpType::Del => OpType::Del,
RawOpType::MarkBegin => {
let name = name.ok_or_else(|| Error::missing_field("mark(name)"))?;
let expand = expand.unwrap_or(false);
let value = if let Some(datatype) = datatype {
let raw_value = value
.ok_or_else(|| Error::missing_field("value"))?
.unwrap_or(ScalarValue::Null);
raw_value.as_datatype(datatype).map_err(|e| {
Error::invalid_value(
Unexpected::Other(e.unexpected.as_str()),
&e.expected.as_str(),
)
})?
} else {
value
.ok_or_else(|| Error::missing_field("value"))?
.unwrap_or(ScalarValue::Null)
};
OpType::mark(name, expand, value)
}
RawOpType::MarkEnd => {
let expand = expand.unwrap_or(true);
OpType::MarkEnd(expand)
}
RawOpType::Set => {
let value = if let Some(datatype) = datatype {
let raw_value = value

View file

@ -15,6 +15,8 @@ impl Serialize for OpType {
OpType::Make(ObjType::Table) => RawOpType::MakeTable,
OpType::Make(ObjType::List) => RawOpType::MakeList,
OpType::Make(ObjType::Text) => RawOpType::MakeText,
OpType::MarkBegin(_) => RawOpType::MarkBegin,
OpType::MarkEnd(_) => RawOpType::MarkEnd,
OpType::Del => RawOpType::Del,
OpType::Inc(_) => RawOpType::Inc,
OpType::Set(_) => RawOpType::Set,

View file

@ -1,10 +1,13 @@
use crate::exid::ExId;
use crate::op_tree::{OpSetMetadata, OpTreeNode};
use crate::types::{Clock, Counter, ElemId, Op, OpId, OpType, ScalarValue};
use fxhash::FxBuildHasher;
use serde::Serialize;
use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
mod blame;
mod insert;
mod keys;
mod keys_at;
@ -17,8 +20,11 @@ mod nth_at;
mod opid;
mod prop;
mod prop_at;
mod raw_spans;
mod seek_op;
mod spans;
pub(crate) use blame::Blame;
pub(crate) use insert::InsertNth;
pub(crate) use keys::Keys;
pub(crate) use keys_at::KeysAt;
@ -31,7 +37,20 @@ pub(crate) use nth_at::NthAt;
pub(crate) use opid::OpIdSearch;
pub(crate) use prop::Prop;
pub(crate) use prop_at::PropAt;
pub(crate) use raw_spans::RawSpans;
pub(crate) use seek_op::SeekOp;
pub(crate) use spans::{Span, Spans};
#[derive(Serialize, Debug, Clone, PartialEq)]
pub struct SpanInfo {
pub id: ExId,
pub time: i64,
pub start: usize,
pub end: usize,
#[serde(rename = "type")]
pub span_type: String,
pub value: ScalarValue,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct CounterData {

View file

@ -0,0 +1,71 @@
use crate::query::{OpSetMetadata, QueryResult, TreeQuery};
use crate::types::{ElemId, Op, OpType, ScalarValue};
use crate::clock::Clock;
use std::collections::HashMap;
use std::fmt::Debug;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Blame<const B: usize> {
pos: usize,
seen: usize,
last_seen: Option<ElemId>,
last_insert: Option<ElemId>,
seen_at_this_mark: Option<ElemId>,
seen_at_last_mark: Option<ElemId>,
ops: Vec<Op>,
points: Vec<Clock>,
changed: bool,
}
impl<const B: usize> Blame<B> {
pub fn new(points: Vec<Clock>) -> Self {
Blame {
pos: 0,
seen: 0,
last_seen: None,
last_insert: None,
seen_at_last_mark: None,
seen_at_this_mark: None,
changed: false,
points: Vec::new(),
ops: Vec::new(),
}
}
}
impl<const B: usize> TreeQuery<B> for Blame<B> {
/*
fn query_node(&mut self, _child: &OpTreeNode<B>) -> QueryResult {
unimplemented!()
}
*/
fn query_element_with_metadata(&mut self, element: &Op, m: &OpSetMetadata) -> QueryResult {
// find location to insert
// mark or set
if element.succ.is_empty() {
if let OpType::MarkBegin(_) = &element.action {
let pos = self
.ops
.binary_search_by(|probe| m.lamport_cmp(probe.id, element.id))
.unwrap_err();
self.ops.insert(pos, element.clone());
}
if let OpType::MarkEnd(_) = &element.action {
self.ops.retain(|op| op.id != element.id.prev());
}
}
if element.insert {
self.last_seen = None;
self.last_insert = element.elemid();
}
if self.last_seen.is_none() && element.visible() {
//self.check_marks();
self.seen += 1;
self.last_seen = element.elemid();
self.seen_at_this_mark = element.elemid();
}
self.pos += 1;
QueryResult::Next
}
}

View file

@ -85,6 +85,10 @@ impl<const B: usize> TreeQuery<B> for InsertNth<B> {
self.last_seen = None;
self.last_insert = element.elemid();
}
if self.valid.is_some() && element.valid_mark_anchor() {
self.last_valid_insert = element.elemid();
self.valid = None;
}
if self.last_seen.is_none() && element.visible() {
if self.seen >= self.target {
return QueryResult::Finish;

View file

@ -0,0 +1,80 @@
use crate::query::{OpSetMetadata, QueryResult, TreeQuery};
use crate::types::{ElemId, Op, OpId, OpType, ScalarValue};
use std::fmt::Debug;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct RawSpans<const B: usize> {
pos: usize,
seen: usize,
last_seen: Option<ElemId>,
last_insert: Option<ElemId>,
changed: bool,
pub spans: Vec<RawSpan>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct RawSpan {
pub id: OpId,
pub change: usize,
pub start: usize,
pub end: usize,
pub name: String,
pub value: ScalarValue,
}
impl<const B: usize> RawSpans<B> {
pub fn new() -> Self {
RawSpans {
pos: 0,
seen: 0,
last_seen: None,
last_insert: None,
changed: false,
spans: Vec::new(),
}
}
}
impl<const B: usize> TreeQuery<B> for RawSpans<B> {
fn query_element_with_metadata(&mut self, element: &Op, m: &OpSetMetadata) -> QueryResult {
// find location to insert
// mark or set
if element.succ.is_empty() {
if let OpType::MarkBegin(md) = &element.action {
let pos = self
.spans
.binary_search_by(|probe| m.lamport_cmp(probe.id, element.id))
.unwrap_err();
self.spans.insert(
pos,
RawSpan {
id: element.id,
change: element.change,
start: self.seen,
end: 0,
name: md.name.clone(),
value: md.value.clone(),
},
);
}
if let OpType::MarkEnd(_) = &element.action {
for s in self.spans.iter_mut() {
if s.id == element.id.prev() {
s.end = self.seen;
break;
}
}
}
}
if element.insert {
self.last_seen = None;
self.last_insert = element.elemid();
}
if self.last_seen.is_none() && element.visible() {
self.seen += 1;
self.last_seen = element.elemid();
}
self.pos += 1;
QueryResult::Next
}
}

View file

@ -0,0 +1,108 @@
use crate::query::{OpSetMetadata, QueryResult, TreeQuery};
use crate::types::{ElemId, Op, OpType, ScalarValue};
use std::collections::HashMap;
use std::fmt::Debug;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Spans<const B: usize> {
pos: usize,
seen: usize,
last_seen: Option<ElemId>,
last_insert: Option<ElemId>,
seen_at_this_mark: Option<ElemId>,
seen_at_last_mark: Option<ElemId>,
ops: Vec<Op>,
marks: HashMap<String, ScalarValue>,
changed: bool,
pub spans: Vec<Span>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Span {
pub pos: usize,
pub marks: Vec<(String, ScalarValue)>,
}
impl<const B: usize> Spans<B> {
pub fn new() -> Self {
Spans {
pos: 0,
seen: 0,
last_seen: None,
last_insert: None,
seen_at_last_mark: None,
seen_at_this_mark: None,
changed: false,
ops: Vec::new(),
marks: HashMap::new(),
spans: Vec::new(),
}
}
pub fn check_marks(&mut self) {
let mut new_marks = HashMap::new();
for op in &self.ops {
if let OpType::MarkBegin(m) = &op.action {
new_marks.insert(m.name.clone(), m.value.clone());
}
}
if new_marks != self.marks {
self.changed = true;
self.marks = new_marks;
}
if self.changed
&& (self.seen_at_last_mark != self.seen_at_this_mark
|| self.seen_at_last_mark.is_none() && self.seen_at_this_mark.is_none())
{
self.changed = false;
self.seen_at_last_mark = self.seen_at_this_mark;
let mut marks: Vec<_> = self
.marks
.iter()
.map(|(key, val)| (key.clone(), val.clone()))
.collect();
marks.sort_by(|(k1, _), (k2, _)| k1.cmp(k2));
self.spans.push(Span {
pos: self.seen,
marks,
});
}
}
}
impl<const B: usize> TreeQuery<B> for Spans<B> {
/*
fn query_node(&mut self, _child: &OpTreeNode<B>) -> QueryResult {
unimplemented!()
}
*/
fn query_element_with_metadata(&mut self, element: &Op, m: &OpSetMetadata) -> QueryResult {
// find location to insert
// mark or set
if element.succ.is_empty() {
if let OpType::MarkBegin(_) = &element.action {
let pos = self
.ops
.binary_search_by(|probe| m.lamport_cmp(probe.id, element.id))
.unwrap_err();
self.ops.insert(pos, element.clone());
}
if let OpType::MarkEnd(_) = &element.action {
self.ops.retain(|op| op.id != element.id.prev());
}
}
if element.insert {
self.last_seen = None;
self.last_insert = element.elemid();
}
if self.last_seen.is_none() && element.visible() {
self.check_marks();
self.seen += 1;
self.last_seen = element.elemid();
self.seen_at_this_mark = element.elemid();
}
self.pos += 1;
QueryResult::Next
}
}

View file

@ -1,7 +1,7 @@
use crate::exid::ExId;
use crate::query::{self, OpIdSearch};
use crate::types::{Key, ObjId, OpId};
use crate::{change::export_change, types::Op, Automerge, ChangeHash, Prop, Value};
use crate::{change::export_change, types::Op, Automerge, ChangeHash, Prop, ScalarValue, Value};
use crate::{AutomergeError, OpType};
#[derive(Debug, Clone)]
@ -116,6 +116,7 @@ impl TransactionInner {
value: V,
) -> Result<Option<ExId>, AutomergeError> {
let obj = doc.exid_to_obj(obj)?;
let value = value.into();
if let Some(id) = self.do_insert(doc, obj, index, value)? {
Ok(Some(doc.id_to_exid(id)))
} else {
@ -123,20 +124,45 @@ impl TransactionInner {
}
}
fn do_insert<V: Into<Value>>(
#[allow(clippy::too_many_arguments)]
pub fn mark(
&mut self,
doc: &mut Automerge,
obj: &ExId,
start: usize,
expand_start: bool,
end: usize,
expand_end: bool,
mark: &str,
value: ScalarValue,
) -> Result<(), AutomergeError> {
let obj = doc.exid_to_obj(obj)?;
self.do_insert(
doc,
obj,
start,
OpType::mark(mark.into(), expand_start, value),
)?;
self.do_insert(doc, obj, end, OpType::MarkEnd(expand_end))?;
Ok(())
}
fn do_insert<V: Into<OpType>>(
&mut self,
doc: &mut Automerge,
obj: ObjId,
index: usize,
value: V,
action: V,
) -> Result<Option<OpId>, AutomergeError> {
let id = self.next_id();
let query = doc.ops.search(obj, query::InsertNth::new(index));
let key = query.key()?;
let value = value.into();
let action = value.into();
//let value = value.into();
let action = action.into();
let is_make = matches!(&action, OpType::Make(_));
let op = Op {

View file

@ -1,6 +1,6 @@
use crate::exid::ExId;
use crate::AutomergeError;
use crate::{Automerge, ChangeHash, Prop, Value};
use crate::{query, Automerge, ChangeHash, Prop, ScalarValue, Value};
use super::{CommitOptions, Transactable, TransactionInner};
@ -106,6 +106,29 @@ impl<'a> Transactable for Transaction<'a> {
.insert(self.doc, obj, index, value)
}
#[allow(clippy::too_many_arguments)]
fn mark(
&mut self,
obj: &ExId,
start: usize,
expand_start: bool,
end: usize,
expand_end: bool,
mark: &str,
value: ScalarValue,
) -> Result<(), AutomergeError> {
self.inner.as_mut().unwrap().mark(
self.doc,
obj,
start,
expand_start,
end,
expand_end,
mark,
value,
)
}
fn inc<P: Into<Prop>>(
&mut self,
obj: &ExId,
@ -158,6 +181,26 @@ impl<'a> Transactable for Transaction<'a> {
self.doc.text_at(obj, heads)
}
fn list(&self, obj: &ExId) -> Result<Vec<(Value, ExId)>, AutomergeError> {
self.doc.list(obj)
}
fn list_at(
&self,
obj: &ExId,
heads: &[ChangeHash],
) -> Result<Vec<(Value, ExId)>, AutomergeError> {
self.doc.list_at(obj, heads)
}
fn spans(&self, obj: &ExId) -> Result<Vec<query::Span>, AutomergeError> {
self.doc.spans(obj)
}
fn raw_spans(&self, obj: &ExId) -> Result<Vec<query::SpanInfo>, AutomergeError> {
self.doc.raw_spans(obj)
}
fn value<P: Into<Prop>>(
&self,
obj: &ExId,

View file

@ -1,5 +1,6 @@
use crate::exid::ExId;
use crate::{AutomergeError, ChangeHash, Prop, Value};
use crate::query;
use crate::{AutomergeError, ChangeHash, Prop, ScalarValue, Value};
use unicode_segmentation::UnicodeSegmentation;
/// A way of mutating a document within a single change.
@ -35,6 +36,19 @@ pub trait Transactable {
value: V,
) -> Result<Option<ExId>, AutomergeError>;
/// Set a mark within a range on a list
#[allow(clippy::too_many_arguments)]
fn mark(
&mut self,
obj: &ExId,
start: usize,
expand_start: bool,
end: usize,
expand_end: bool,
mark: &str,
value: ScalarValue,
) -> Result<(), AutomergeError>;
/// Increment the counter at the prop in the object by `value`.
fn inc<P: Into<Prop>>(&mut self, obj: &ExId, prop: P, value: i64)
-> Result<(), AutomergeError>;
@ -85,6 +99,22 @@ pub trait Transactable {
/// Get the string that this text object represents at a point in history.
fn text_at(&self, obj: &ExId, heads: &[ChangeHash]) -> Result<String, AutomergeError>;
/// Get the string that this text object represents.
fn list(&self, obj: &ExId) -> Result<Vec<(Value, ExId)>, AutomergeError>;
/// Get the string that this text object represents at a point in history.
fn list_at(
&self,
obj: &ExId,
heads: &[ChangeHash],
) -> Result<Vec<(Value, ExId)>, AutomergeError>;
/// test spans api for mark/span experiment
fn spans(&self, obj: &ExId) -> Result<Vec<query::Span>, AutomergeError>;
/// test raw_spans api for mark/span experiment
fn raw_spans(&self, obj: &ExId) -> Result<Vec<query::SpanInfo>, AutomergeError>;
/// Get the value at this prop in the object.
fn value<P: Into<Prop>>(
&self,

View file

@ -158,6 +158,25 @@ pub enum OpType {
Del,
Inc(i64),
Set(ScalarValue),
MarkBegin(MarkData),
MarkEnd(bool),
}
impl OpType {
pub(crate) fn mark(name: String, expand: bool, value: ScalarValue) -> Self {
OpType::MarkBegin(MarkData {
name,
expand,
value,
})
}
}
#[derive(PartialEq, Debug, Clone)]
pub struct MarkData {
pub name: String,
pub value: ScalarValue,
pub expand: bool,
}
#[derive(Debug)]
@ -180,6 +199,10 @@ impl OpId {
pub fn actor(&self) -> usize {
self.1
}
#[inline]
pub fn prev(&self) -> OpId {
OpId(self.0 - 1, self.1)
}
}
impl Exportable for ObjId {
@ -376,7 +399,7 @@ impl Op {
}
pub fn visible(&self) -> bool {
if self.is_inc() {
if self.is_inc() || self.is_mark() {
false
} else if self.is_counter() {
self.succ.len() <= self.incs()
@ -401,6 +424,18 @@ impl Op {
matches!(&self.action, OpType::Inc(_))
}
pub fn valid_mark_anchor(&self) -> bool {
self.succ.is_empty()
&& matches!(
&self.action,
OpType::MarkBegin(MarkData { expand: true, .. }) | OpType::MarkEnd(false)
)
}
pub fn is_mark(&self) -> bool {
matches!(&self.action, OpType::MarkBegin(_) | OpType::MarkEnd(_))
}
pub fn is_counter(&self) -> bool {
matches!(&self.action, OpType::Set(ScalarValue::Counter(_)))
}
@ -435,6 +470,8 @@ impl Op {
OpType::Set(value) if self.insert => format!("i:{}", value),
OpType::Set(value) => format!("s:{}", value),
OpType::Make(obj) => format!("make{}", obj),
OpType::MarkBegin(m) => format!("mark{}={}", m.name, m.value),
OpType::MarkEnd(_) => "unmark".into(),
OpType::Inc(val) => format!("inc:{}", val),
OpType::Del => "del".to_string(),
}

View file

@ -18,6 +18,13 @@ impl Value {
}
}
pub fn as_string(&self) -> Option<String> {
match self {
Value::Scalar(val) => val.as_string(),
_ => None,
}
}
pub fn map() -> Value {
Value::Object(ObjType::Map)
}
@ -394,6 +401,13 @@ impl ScalarValue {
}
}
pub fn as_string(&self) -> Option<String> {
match self {
ScalarValue::Str(s) => Some(s.to_string()),
_ => None,
}
}
pub fn counter(n: i64) -> ScalarValue {
ScalarValue::Counter(n.into())
}