Skip to content

Commit

Permalink
Merge pull request #142 from cloudpeers/orarray-struct-bug
Browse files Browse the repository at this point in the history
ORArray: Correctly handle nested structs
  • Loading branch information
wngr authored Feb 7, 2022
2 parents eb66d7d + d462a4d commit a36b65c
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 75 deletions.
17 changes: 12 additions & 5 deletions api/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ const setValue = (cursor: Cursor, value: any): Causal => {
value.forEach((v, idx) => {
const here = cursor.clone()
here.arrayIndex(idx)
const c = setPrimitiveValue(here, v)
const c = setValue(here, v)
if (causal) {
causal.join(c)
} else {
Expand Down Expand Up @@ -130,7 +130,7 @@ const setValue = (cursor: Cursor, value: any): Causal => {
} else {
here.structField(k)
}
const c = setPrimitiveValue(here, v)
const c = setValue(here, v)
if (causal) {
causal.join(c)
} else {
Expand Down Expand Up @@ -167,7 +167,8 @@ const setPrimitiveValue = (cursor: Cursor, value: any): Causal => {
case "Reg<string>":
return cursor.regAssignStr(value.toString())
default: {
throw new Error("unreachable")
throw new Error(`Unknown type: ${cursor.typeOf()}; value: ${value}`)

}
}
}
Expand All @@ -192,8 +193,14 @@ const mkProxy = <T extends object>(doc: Doc, cursor_?: Cursor): T => {
case "valueOf":
return undefined
case "toString": {
throw new Error(`Method ${p} not implemented in TLFS proxy. Please file a bug and/or use the document API directly to work around.`)

return function () { return 'FIXME' }


}
// default:
// throw new Error(`Method ${String(p)} not implemented in TLFS proxy. Please file a bug and/or use the document API directly to work around.`)

}

const cursor = cursor_?.clone() || doc.createCursor()
Expand All @@ -204,7 +211,7 @@ const mkProxy = <T extends object>(doc: Doc, cursor_?: Cursor): T => {
switch (p) {
case 'filter': {
return function (...args: any[]) {
const arr = new Array(cursor.arrayLength()).map((_v, idx) =>
const arr = new Array(cursor.arrayLength()).fill(undefined).map((_v, idx) =>
get(doc, cursor.clone())({}, idx.toString(), undefined)
)

Expand Down
4 changes: 2 additions & 2 deletions api/js/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion api/js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tlfs",
"version": "0.1.4",
"version": "0.1.8",
"description": "The local first SDK",
"main": "lib/index.js",
"module": "lib/index.js",
Expand Down
51 changes: 48 additions & 3 deletions crdt/src/crdt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -756,31 +756,76 @@ mod tests {

let mut cur = doc.cursor();
cur.index(0)?.field("title")?;
let op = cur.assign_str("something that needs to be done")?;
let op = cur.assign_str("first todo")?;
doc.apply(&op)?;

let mut cur = doc.cursor();
cur.index(1)?.field("title")?;
let op = cur.assign_str("second todo")?;
doc.apply(&op)?;

assert_eq!(doc.cursor().len()?, 2);
let r = doc
.cursor()
.index(0)?
.field("title")?
.strs()?
.collect::<Result<Vec<_>>>()?;
assert_eq!(r.len(), 1);
assert_eq!(r[0], "something that needs to be done");
assert_eq!(r[0], "first todo");

let mut cur = doc.cursor();
cur.index(1)?.field("complete")?;
assert!(!cur.enabled()?);

let mut cur = doc.cursor();
cur.index(0)?.field("complete")?;
let op = cur.enable()?;
doc.apply(&op)?;

let mut cur = doc.cursor();
cur.index(0)?.field("complete")?;
assert!(cur.enabled()?);

// second stays disabled
let mut cur = doc.cursor();
cur.index(1)?.field("complete")?;
assert!(!cur.enabled()?);

for (idx, v) in [(0, "first todo"), (1, "second todo")] {
assert_eq!(doc.cursor().len()?, 2);
let r = doc
.cursor()
.index(idx)?
.field("title")?
.strs()?
.collect::<Result<Vec<_>>>()?;
assert_eq!(r.len(), 1);
assert_eq!(r[0], v);
}

let mut cur = doc.cursor();
let op = cur.index(0)?.delete()?;
doc.apply(&op)?;

assert_eq!(doc.cursor().len()?, 1);
let r = doc
.cursor()
.index(0)?
.field("title")?
.strs()?
.collect::<Result<Vec<_>>>()?;
assert_eq!(r.len(), 1);
assert_eq!(r[0], "something that needs to be done");
assert_eq!(r[0], "second todo");

let r = doc
.cursor()
.index(1)?
.field("title")?
.strs()?
.collect::<Result<Vec<_>>>()?;
assert!(r.is_empty());

Ok(())
}

Expand Down
132 changes: 68 additions & 64 deletions crdt/src/cursor.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::{BTreeMap, BTreeSet};

use crate::acl::{Actor, Can, Permission, Policy};
use crate::crdt::{Causal, Crdt, DotStore};
use crate::crypto::Keypair;
Expand Down Expand Up @@ -283,13 +285,28 @@ impl<'a> Cursor<'a> {
if let ArchivedSchema::Array(_) = &self.schema {
let mut path = self.path.clone();
path.prim_str(array_util::ARRAY_VALUES);
let res = self.count_path(path.as_path());
res

let res = self.pos_iter(Some(path)).collect::<BTreeSet<_>>().len();
Ok(res as u32)
} else {
anyhow::bail!("not an Array<_>");
}
}

fn pos_iter(&self, path: Option<PathBuf>) -> impl Iterator<Item = Fraction> + '_ {
let path = path.unwrap_or_else(|| self.path.clone());
self.crdt.scan_path(path.as_path()).filter_map(move |e| {
let p = Path::new(&e);
p.strip_prefix(path.as_path())
.ok()
.and_then(|e| e.first())
.and_then(|x| match x {
crate::Segment::Position(x) => Some(x),
_ => None,
})
})
}

/// Returns if the array is empty.
pub fn is_empty(&mut self) -> Result<bool> {
Ok(self.len()? == 0)
Expand All @@ -310,14 +327,6 @@ impl<'a> Cursor<'a> {
}
}

fn count_path(&self, path: Path) -> Result<u32> {
let mut i = 0;
for _ in self.crdt.scan_path(path) {
i += 1;
}
Ok(i)
}

fn nonce(&self, path: &mut PathBuf) {
path.nonce(nonce());
}
Expand Down Expand Up @@ -579,55 +588,63 @@ impl ArrayWrapper {
}
}

fn new(cursor: &Cursor, mut ix: usize) -> Result<(Self, PathBuf)> {
let array_path = cursor.path.clone();
fn arr_items(
cursor: &Cursor,
array_root: PathBuf,
) -> impl Iterator<Item = Result<array_util::ArrayValueEntry>> {
let array_value_root = {
let mut p = array_path.clone();
let mut p = array_root.clone();
p.prim_str(array_util::ARRAY_VALUES);
p
};
// TODO: use sled's size hint
let len = cursor.crdt.scan_path(array_value_root.as_path()).count();
let mut iter = cursor.crdt.scan_path(array_value_root.as_path());

cursor
.crdt
.scan_path(array_value_root.as_path())
.map(move |val| {
array_util::ArrayValueEntry::from_path(
Path::new(&val).strip_prefix(array_root.as_path())?,
)
.context("Reading array data")
})
}

fn distinct_arr_items(
cursor: &Cursor,
array_root: PathBuf,
) -> impl Iterator<Item = Result<(Fraction, u64)>> {
let mut scan_state = BTreeMap::default();
Self::arr_items(cursor, array_root).filter_map(move |val| match val {
Ok(data) => match scan_state.insert(data.pos.clone(), data.uid) {
Some(existing) if existing != data.uid => unreachable!(),
Some(_) => None,
None => Some(Ok((data.pos, data.uid))),
},
Err(e) => Some(Err(e)),
})
}

fn new(cursor: &Cursor, mut ix: usize) -> Result<(Self, PathBuf)> {
let array_path = cursor.path.clone();

let len = Self::distinct_arr_items(cursor, cursor.path.clone()).count();
let mut iter = Self::distinct_arr_items(cursor, cursor.path.clone());

ix = ix.min(len);
let (pos, uid) = if let Some(entry) = iter.nth(ix) {
// Existing entry
let p_c = cursor.path.clone();
let data = array_util::ArrayValueEntry::from_path(
Path::new(&entry).strip_prefix(p_c.as_path())?,
)
.context("Reading array data")?;
(data.pos, data.uid)
entry?
} else {
let p_c = cursor.path.clone();
// No entry, find position to insert
let (left, right) = match ix.checked_sub(1) {
Some(s) => {
let p_c = cursor.path.clone();
let mut iter = cursor
.crdt
.scan_path(array_value_root.as_path())
let mut iter = Self::distinct_arr_items(cursor, p_c)
.skip(s)
.map(move |iv| -> anyhow::Result<_> {
let meta = array_util::ArrayValueEntry::from_path(
Path::new(&iv).strip_prefix(p_c.as_path())?,
)?;
Ok(meta.pos)
});
.map(|v| v.map(|(p, _)| p));
(iter.next(), iter.next())
}
None => {
let p_c = cursor.path.clone();
let mut iter =
cursor
.crdt
.scan_path(array_value_root.as_path())
.map(move |iv| {
let meta = array_util::ArrayValueEntry::from_path(
Path::new(&iv).strip_prefix(p_c.as_path())?,
)?;
Ok(meta.pos)
});
let mut iter = Self::distinct_arr_items(cursor, p_c).map(|v| v.map(|(p, _)| p));

(None, iter.next())
}
Expand Down Expand Up @@ -674,33 +691,19 @@ impl ArrayWrapper {
// child tree to all roots with the new position.

let new_pos = {
// TODO: use sled's size hint
let mut value_path = self.array_path.clone();
value_path.prim_str(array_util::ARRAY_VALUES);
let len = cursor.crdt.scan_path(value_path.as_path()).count();
let len = Self::distinct_arr_items(cursor, self.array_path.clone()).count();

to = to.min(len);
let p_c = self.array_path.clone();
let (left, right) = match to.checked_sub(1) {
Some(s) => {
let p_c = self.array_path.clone();
let mut iter = cursor.crdt.scan_path(value_path.as_path()).skip(s).map(
move |iv| -> anyhow::Result<_> {
let meta = array_util::ArrayValueEntry::from_path(
Path::new(&iv).strip_prefix(p_c.as_path())?,
)?;
Ok(meta.pos)
},
);
let mut iter = Self::distinct_arr_items(cursor, p_c)
.skip(s)
.map(|v| v.map(|(p, _)| p));
(iter.next(), iter.next())
}
None => {
let p_c = self.array_path.clone();
let mut iter = cursor.crdt.scan_path(value_path.as_path()).map(move |iv| {
let meta = array_util::ArrayValueEntry::from_path(
Path::new(&iv).strip_prefix(p_c.as_path())?,
)?;
Ok(meta.pos)
});
let mut iter = Self::distinct_arr_items(cursor, p_c).map(|v| v.map(|(p, _)| p));

(None, iter.next())
}
Expand Down Expand Up @@ -844,6 +847,7 @@ mod array_util {
pub(crate) const ARRAY_META: &str = "META";

// <path_to_array>.VALUES.<pos>.<uid>.<value>
#[derive(Debug)]
pub(crate) struct ArrayValueEntry {
pub(crate) pos: Fraction,
pub(crate) uid: u64,
Expand Down

0 comments on commit a36b65c

Please sign in to comment.