diff --git a/.gitmodules b/.gitmodules index 5241fb2f246d8096035c6d18118a02a5b4455190..570f2dbc0c113b5f2531a456c91dba0753aa42f1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "vec-map"] path = vec-map - url = git@gitlab.doc.ic.ac.uk:ml5717/vec-map.git + url = git@gitlab.doc.ic.ac.uk:sweng-group-15/vec-map.git diff --git a/Cargo.toml b/Cargo.toml index e65b2c51648e0d225f9c160dff33d34015b3aadc..47a00165152be5e1990c82ed3825a9182be649e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,9 @@ serde-wasm-bindgen = "0.1.3" vec_map = { path = "vec-map", features = ["serde"] } bincode = "1.2.1" fixedbitset = "0.2.0" -# web-sys = { version = "0.3.33", features = ["console"] } +ordered-float = { version = "1.0.2", features = ["serde"] } +twox-hash = "1.1.1" +web-sys = { version = "0.3.33", features = ["console"] } # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires diff --git a/src/crdt.rs b/src/crdt.rs index 63248628884c4a3df26bcc23d9f119263bf7233c..30fae914851471f35f55293e90ea730221b097b8 100644 --- a/src/crdt.rs +++ b/src/crdt.rs @@ -1,4 +1,5 @@ use fixedbitset::FixedBitSet; +use ordered_float::NotNan; use serde::de::{self, Deserialize, Deserializer, SeqAccess, Visitor}; use serde::ser::{Serialize, SerializeSeq, SerializeStruct, SerializeTuple, Serializer}; use std::cmp; @@ -6,14 +7,17 @@ use std::collections::hash_map::Entry::{Occupied, Vacant}; use std::collections::HashMap; use std::convert::TryInto; use std::fmt; +use std::hash::{Hash, Hasher}; use std::ops::{Deref, DerefMut}; +use twox_hash::XxHash64 as XXHasher; use uuid::Uuid; use vec_map::VecMap; #[derive(Debug)] struct Dirty<T> { value: T, - dirty: bool, + dirty_events: bool, + dirty_state: bool, } impl<T> Deref for Dirty<T> { @@ -32,19 +36,32 @@ impl<T> DerefMut for Dirty<T> { impl<T> Dirty<T> { pub fn new(value: T, dirty: bool) -> Dirty<T> { - Dirty { value, dirty } + Dirty { + value, + dirty_events: dirty, + dirty_state: dirty, + } + } + + pub fn has_dirty_events(&self) -> bool { + self.dirty_events } - pub fn is_dirty(&self) -> bool { - self.dirty + pub fn has_dirty_state(&self) -> bool { + self.dirty_state } - pub fn clean(&mut self) { - self.dirty = false + pub fn clean_events(&mut self) { + self.dirty_events = false; + } + + pub fn clean_state(&mut self) { + self.dirty_state = false; } pub fn dirty(&mut self) { - self.dirty = true + self.dirty_events = true; + self.dirty_state = true; } } @@ -146,31 +163,28 @@ impl Point { } } -pub trait IntervalBound { - fn max(a: Self, b: Self) -> Self; +pub trait IntervalBound: Copy + Clone + Default + Ord + Hash { fn up(self) -> Self; fn down(self) -> Self; + fn sub(self, nom: u8, denom: u8) -> Self; } -impl IntervalBound for f32 { - fn max(a: f32, b: f32) -> f32 { - f32::max(a, b) +impl IntervalBound for NotNan<f32> { + fn up(self) -> NotNan<f32> { + self } - fn up(self) -> f32 { + fn down(self) -> NotNan<f32> { self } - fn down(self) -> f32 { - self + fn sub(self, nom: u8, denom: u8) -> NotNan<f32> { + self * unsafe { NotNan::unchecked_new(nom as f32) } + / unsafe { NotNan::unchecked_new(denom as f32) } } } impl IntervalBound for usize { - fn max(a: usize, b: usize) -> usize { - cmp::max(a, b) - } - fn up(self) -> usize { self + 1 } @@ -178,12 +192,16 @@ impl IntervalBound for usize { fn down(self) -> usize { self - 1 } + + fn sub(self, nom: u8, denom: u8) -> usize { + self * (nom as usize) / (denom as usize) + } } -#[derive(Copy, Clone, PartialEq, Debug)] +#[derive(Copy, Clone, PartialEq, Hash, Debug)] pub struct Interval<T> where - T: Copy + Clone + PartialEq + PartialOrd + IntervalBound, + T: IntervalBound, { from: T, to: T, @@ -191,21 +209,21 @@ where impl<T> Interval<T> where - T: Copy + Clone + PartialEq + PartialOrd + IntervalBound, + T: IntervalBound, { fn new(from: T, to: T) -> Interval<T> { Interval { from, to } } } -#[derive(Clone, Debug)] +#[derive(Clone, Hash, Debug)] pub struct IntervalUnion<T>(Vec<Interval<T>>) where - T: Copy + Clone + PartialEq + PartialOrd + IntervalBound; + T: IntervalBound; impl<T> Serialize for IntervalUnion<T> where - T: Copy + Clone + PartialEq + PartialOrd + IntervalBound + Serialize, + T: IntervalBound + Serialize, { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where @@ -224,7 +242,7 @@ where impl<'de, T> Deserialize<'de> for IntervalUnion<T> where - T: Copy + Clone + PartialEq + PartialOrd + IntervalBound + Deserialize<'de>, + T: IntervalBound + Deserialize<'de>, { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where @@ -234,19 +252,19 @@ where struct IntervalUnionVisitor<T> where - T: Copy + Clone + PartialEq + PartialOrd + IntervalBound, + T: IntervalBound, { marker: PhantomData<fn() -> IntervalUnion<T>>, }; impl<'de, T> Visitor<'de> for IntervalUnionVisitor<T> where - T: Copy + Clone + PartialEq + PartialOrd + IntervalBound + Deserialize<'de>, + T: IntervalBound + Deserialize<'de>, { type Value = IntervalUnion<T>; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("struct IntervalUnion") + formatter.write_str("struct IntervalUnion<T: IntervalBound>") } fn visit_seq<V>(self, mut seq: V) -> Result<Self::Value, V::Error> @@ -274,7 +292,7 @@ where impl<T> IntervalUnion<T> where - T: Copy + Clone + PartialEq + PartialOrd + IntervalBound, + T: IntervalBound, { fn new() -> IntervalUnion<T> { IntervalUnion(Vec::new()) @@ -284,6 +302,17 @@ where self.0.is_empty() } + fn is_full(&self) -> bool { + match self.0.first() { + Some(interval) if self.0.len() == 1 => interval.from == T::default(), + _ => false, + } + } + + fn max(&self) -> Option<T> { + self.0.last().map(|interval| interval.to) + } + fn union(&mut self, other: &IntervalUnion<T>) -> bool { if other.is_empty() { return false; @@ -308,7 +337,7 @@ where match intervals.last_mut() { Some(top) => { if interval.from <= top.to { - top.to = T::max(top.to, interval.to) + top.to = cmp::max(top.to, interval.to) } else { intervals.push(interval) } @@ -326,7 +355,7 @@ where match intervals.last_mut() { Some(top) => { if interval.from <= top.to { - top.to = T::max(top.to, interval.to) + top.to = cmp::max(top.to, interval.to) } else { intervals.push(interval) } @@ -385,7 +414,7 @@ where impl<T> IntoIterator for IntervalUnion<T> where - T: Copy + Clone + PartialEq + PartialOrd + IntervalBound, + T: IntervalBound, { type Item = Interval<T>; type IntoIter = std::vec::IntoIter<Self::Item>; @@ -398,7 +427,7 @@ where impl<T> From<Interval<T>> for IntervalUnion<T> where - T: Copy + Clone + PartialEq + PartialOrd + IntervalBound, + T: IntervalBound, { fn from(interval: Interval<T>) -> Self { IntervalUnion(vec![interval]) @@ -409,7 +438,7 @@ where struct Stroke { index: usize, points: Dirty<VecMap<Point>>, - intervals: Dirty<IntervalUnion<f32>>, + intervals: Dirty<IntervalUnion<NotNan<f32>>>, } impl Stroke { @@ -425,7 +454,7 @@ impl Stroke { #[derive(Debug)] pub struct StrokeDelta { points: HashMap<usize, Point>, - intervals: IntervalUnion<f32>, + intervals: IntervalUnion<NotNan<f32>>, } impl Serialize for StrokeDelta { @@ -503,7 +532,7 @@ impl User { } } -#[derive(Hash, Eq, PartialEq, Debug)] +#[derive(Clone, Hash, Eq, PartialEq, Debug)] pub struct StrokeID(u128, usize); impl Serialize for StrokeID { @@ -563,10 +592,211 @@ impl StrokeID { } } +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct IntervalUnionState<T> +where + T: IntervalBound, +{ + hash: u64, + footprint: u64, + range: T, +} + +impl<T> Serialize for IntervalUnionState<T> +where + T: IntervalBound + Serialize, +{ + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + let mut tuple = serializer.serialize_tuple(3)?; + + tuple.serialize_element(&self.hash)?; + tuple.serialize_element(&self.footprint)?; + tuple.serialize_element(&self.range)?; + + tuple.end() + } +} + +impl<'de, T> Deserialize<'de> for IntervalUnionState<T> +where + T: IntervalBound + Deserialize<'de>, +{ + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + use std::marker::PhantomData; + + struct IntervalUnionStateVisitor<T> + where + T: IntervalBound, + { + marker: PhantomData<fn() -> IntervalUnionState<T>>, + }; + + impl<'de, T> Visitor<'de> for IntervalUnionStateVisitor<T> + where + T: IntervalBound + Deserialize<'de>, + { + type Value = IntervalUnionState<T>; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct IntervalUnionState<T: IntervalBound>") + } + + fn visit_seq<V>(self, mut seq: V) -> Result<Self::Value, V::Error> + where + V: SeqAccess<'de>, + { + let hash = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(0, &self))?; + let footprint = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(1, &self))?; + let range = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(2, &self))?; + + Ok(IntervalUnionState { + hash, + footprint, + range, + }) + } + } + + let visitor = IntervalUnionStateVisitor { + marker: PhantomData, + }; + + deserializer.deserialize_tuple(3, visitor) + } +} + +impl<T> Into<IntervalUnion<T>> for IntervalUnionState<T> +where + T: IntervalBound, +{ + fn into(self) -> IntervalUnion<T> { + let mut marker = IntervalUnionState::<T>::FOOTPRINT_MARKER; + + let mut intervals = Vec::new(); + + let footprint = self.footprint.wrapping_neg(); + + if footprint == IntervalUnionState::<T>::FULL_INTERVALS { + intervals.push(Interval::new(T::default(), self.range)); + } else if footprint != IntervalUnionState::<T>::EMPTY_INTERVALS { + for from in 0u8..64u8 { + if (self.footprint & marker) != IntervalUnionState::<T>::EMPTY_INTERVALS { + let interval = + Interval::new(self.range.sub(from, 64u8), self.range.sub(from + 1u8, 64u8)); + + match intervals.last_mut() { + Some(last) if last.to.up() == interval.from => last.to = interval.to, + _ => intervals.push(interval), + }; + } + + marker >>= 1; + } + } + + IntervalUnion(intervals) + } +} + +impl<T> IntervalUnionState<T> +where + T: IntervalBound, +{ + const EMPTY_INTERVALS: u64 = std::u64::MIN; + const EMPTY_CHICKEN_INTERVALS: u64 = 0x3482b3a70bdbe3c0u64; + + const FULL_INTERVALS: u64 = std::u64::MAX; + const FULL_CHICKEN_INTERVALS: u64 = 0x05b4d9adff1491aau64; + + const FOOTPRINT_MARKER: u64 = 0x8000000000000000u64; + + fn new(intervals: &IntervalUnion<T>) -> IntervalUnionState<T> { + IntervalUnionState { + hash: Self::hash(intervals), + footprint: Self::footprint(intervals), + range: Self::range(intervals), + } + } + + fn hash(intervals: &IntervalUnion<T>) -> u64 { + if intervals.is_empty() { + Self::EMPTY_INTERVALS + } else if intervals.is_full() { + Self::FULL_INTERVALS + } else { + let mut hasher = XXHasher::default(); + intervals.hash(&mut hasher); + + match hasher.finish() { + Self::EMPTY_INTERVALS => Self::EMPTY_CHICKEN_INTERVALS, + Self::FULL_INTERVALS => Self::FULL_CHICKEN_INTERVALS, + hash => hash, + } + } + .wrapping_neg() + } + + fn range(intervals: &IntervalUnion<T>) -> T { + intervals.max().unwrap_or(T::default()) + } + + fn footprint(intervals: &IntervalUnion<T>) -> u64 { + if intervals.is_empty() { + Self::EMPTY_INTERVALS + } else if intervals.is_full() { + Self::FULL_INTERVALS + } else { + let range = Self::range(intervals); + + let mut si = 0u8; + let mut oi = 0; + + let mut current = Interval::new(range.sub(si, 64u8), range.sub(si + 1u8, 64u8)); + + let mut footprint = Self::EMPTY_INTERVALS; + let mut marker = Self::FOOTPRINT_MARKER; + + while si < 64u8 && oi < intervals.0.len() { + if intervals.0[oi].to < current.from { + oi += 1; + continue; + } + + if intervals.0[oi].from <= current.from { + if intervals.0[oi].to >= current.to { + footprint |= marker; + } else { + oi += 1; + } + } + + marker >>= 1; + si += 1u8; + current = Interval::new(range.sub(si, 64u8), range.sub(si + 1u8, 64u8)); + } + + footprint + } + .wrapping_neg() + } +} + #[derive(Debug)] pub struct StateVec { strokes: HashMap<u128, IntervalUnion<usize>>, - //intervals: HashMap<>, + intervals: HashMap<StrokeID, IntervalUnionState<NotNan<f32>>>, } impl Serialize for StateVec { @@ -574,9 +804,10 @@ impl Serialize for StateVec { where S: Serializer, { - let mut tuple = serializer.serialize_tuple(1)?; + let mut tuple = serializer.serialize_tuple(2)?; tuple.serialize_element(&self.strokes)?; + tuple.serialize_element(&self.intervals)?; tuple.end() } @@ -603,12 +834,15 @@ impl<'de> Deserialize<'de> for StateVec { let strokes = seq .next_element()? .ok_or_else(|| de::Error::invalid_length(0, &self))?; + let intervals = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(1, &self))?; - Ok(StateVec { strokes }) + Ok(StateVec { strokes, intervals }) } } - deserializer.deserialize_tuple(1, StateVecVisitor) + deserializer.deserialize_tuple(2, StateVecVisitor) } } @@ -616,13 +850,14 @@ impl StateVec { fn new() -> StateVec { StateVec { strokes: HashMap::new(), + intervals: HashMap::new(), } } } pub trait EventListener { fn on_stroke(&self, stroke: String, points: &VecMap<Point>); - fn on_interval(&self, stroke: String, intervals: &IntervalUnion<f32>); + fn on_interval(&self, stroke: String, intervals: &IntervalUnion<NotNan<f32>>); fn on_deltas(&self, deltas: HashMap<StrokeID, StrokeDelta>); fn on_deltas_from_state(&self, user: String, deltas: HashMap<StrokeID, StrokeDelta>); @@ -635,10 +870,15 @@ pub struct CRDT { crdt: Dirty<HashMap<u128, Dirty<User>>>, deltas: HashMap<StrokeID, StrokeDelta>, + state: StateVec, + event_listener: Box<dyn EventListener>, } impl CRDT { + const UUID_MASK: u128 = 0xFFFFFFFF_FFFF_0FFF_0FFF_FFFFFFFFFFFFu128; + const UUID_XOR: u128 = 0x00000000_0000_C000_3000_000000000000u128; + pub fn new(event_listener: Box<dyn EventListener>) -> CRDT { CRDT { user: None, @@ -654,6 +894,8 @@ impl CRDT { crdt: Dirty::new(HashMap::new(), false), deltas: HashMap::new(), + state: StateVec::new(), + event_listener, } } @@ -684,13 +926,31 @@ impl CRDT { pub fn canonicalise_user(uuid: &str) -> Option<String> { match Uuid::parse_str(uuid) { Ok(uuid) => Some(CRDT::uuid_to_string( - (uuid.as_u128() & 0xFFFFFFFF_FFFF_0FFF_0FFF_FFFFFFFFFFFFu128) - | 0x00000000_0000_C000_3000_000000000000u128, + CRDT::canonicalise_uuid(uuid.as_u128()).0, )), Err(_) => None, } } + fn canonicalise_uuid(uuid: u128) -> (u128, u8) { + let canonical = (uuid & CRDT::UUID_MASK) | CRDT::UUID_XOR; + + let active_id = { + let xor = uuid ^ CRDT::UUID_XOR; + (((xor >> 72) as u8) & 0xF0u8) | (((xor >> 60) as u8) & 0x0Fu8) + }; + + (canonical, active_id) + } + + fn decanonicalise_uuid(uuid: u128, active_id: u8) -> u128 { + let canonical = (uuid & CRDT::UUID_MASK) | CRDT::UUID_XOR; + let active_id = + (((active_id & 0xF0u8) as u128) << 72) | (((active_id & 0x0Fu8) as u128) << 60); + + canonical ^ active_id + } + pub fn canonicalise_stroke_author(stroke_id: &str) -> Option<String> { let (author, stroke) = match stroke_id.rfind('-') { Some(split) => stroke_id.split_at(split), @@ -720,8 +980,7 @@ impl CRDT { None => return None, }; - let user = user - ^ ((((active_id & 0xF0u8) as u128) << 72) | (((active_id & 0x0Fu8) as u128) << 60)); + let user = CRDT::decanonicalise_uuid(user, active_id); let point = match Point::try_new(x, y, weight, colour) { Some(point) => point, @@ -886,6 +1145,11 @@ impl CRDT { return false; }; + let (from, to) = match (NotNan::new(from), NotNan::new(to)) { + (Ok(from), Ok(to)) => (from, to), + _ => return false, + }; + let (entry, user, stroke_idx) = match Self::split_stroke_id_mut(&mut self.crdt, stroke_id) { Some(stroke) => stroke, None => return false, @@ -932,7 +1196,7 @@ impl CRDT { entry.strokes.get(idx).map(|stroke| stroke.points.deref()) } - pub fn get_stroke_intervals(&self, stroke_id: &str) -> Option<&IntervalUnion<f32>> { + pub fn get_stroke_intervals(&self, stroke_id: &str) -> Option<&IntervalUnion<NotNan<f32>>> { let (entry, _user, stroke_idx) = match Self::split_stroke_id(&self.crdt, stroke_id) { Some(stroke) => stroke, None => return None, @@ -950,32 +1214,32 @@ impl CRDT { } pub fn fetch_events(&mut self) { - if !self.crdt.is_dirty() { + if !self.crdt.has_dirty_events() { return; }; - self.crdt.clean(); + self.crdt.clean_events(); let mut stroke_events = Vec::new(); let mut interval_events = Vec::new(); for (user, entry) in self.crdt.deref_mut() { - if !entry.is_dirty() { - return; + if !entry.has_dirty_events() { + continue; }; - entry.clean(); + entry.clean_events(); let user = CRDT::uuid_to_string(*user) + "-"; for (i, stroke) in entry.strokes.iter_mut().enumerate() { - if stroke.points.is_dirty() { - stroke.points.clean(); + if stroke.points.has_dirty_events() { + stroke.points.clean_events(); stroke_events.push((user.clone() + &i.to_string(), stroke.points.deref())); } - if stroke.intervals.is_dirty() { - stroke.intervals.clean(); + if stroke.intervals.has_dirty_events() { + stroke.intervals.clean_events(); interval_events.push((user.clone() + &i.to_string(), stroke.intervals.deref())); } } @@ -998,8 +1262,6 @@ impl CRDT { let mut deltas = HashMap::new(); std::mem::swap(&mut self.deltas, &mut deltas); - //web_sys::console::log_1(&format!("deltas {:?}", deltas).into()); - self.event_listener.on_deltas(deltas); } @@ -1041,37 +1303,77 @@ impl CRDT { } } - pub fn get_state_vector(&self) -> StateVec { - let mut state = StateVec::new(); - state.strokes.reserve(self.crdt.len()); + pub fn get_state_vector(&mut self) -> &StateVec { + if !self.crdt.has_dirty_state() { + return &self.state; + }; + + self.crdt.clean_state(); + + for (user, entry) in self.crdt.deref_mut() { + if !entry.has_dirty_state() { + continue; + }; + + entry.clean_state(); - for (user, entry) in self.crdt.deref() { let mut point_intervals: Vec<Interval<usize>> = Vec::new(); - for stroke in &entry.strokes { - for point_idx in stroke.points.keys() { - match point_intervals.last_mut() { - Some(interval) if interval.to.up() == (stroke.index + point_idx) => { - interval.to = stroke.index + point_idx - } - _ => point_intervals.push(Interval::new( - stroke.index + point_idx, - stroke.index + point_idx, - )), - }; + for stroke in &mut entry.strokes { + if stroke.points.has_dirty_state() { + stroke.points.clean_state(); + + for point_idx in stroke.points.keys() { + match point_intervals.last_mut() { + Some(interval) if interval.to.up() == (stroke.index + point_idx) => { + interval.to = stroke.index + point_idx + } + _ => point_intervals.push(Interval::new( + stroke.index + point_idx, + stroke.index + point_idx, + )), + }; + } + } + + if stroke.intervals.has_dirty_state() { + stroke.intervals.clean_state(); + + if !stroke.intervals.is_empty() { + self.state.intervals.insert( + StrokeID::new(*user, stroke.index), + IntervalUnionState::new(&stroke.intervals), + ); + } } } - state.strokes.insert(*user, IntervalUnion(point_intervals)); + if !point_intervals.is_empty() { + let point_intervals = IntervalUnion(point_intervals); + + match self.state.strokes.entry(*user) { + Occupied(mut entry) => { + entry.get_mut().union(&point_intervals); + } + Vacant(entry) => { + entry.insert(point_intervals); + } + } + } } - state + &self.state } - pub fn fetch_deltas_from_state_vector(&self, user: String, remote_state: &StateVec) { + pub fn fetch_deltas_from_state_vector(&mut self, user: String, remote_state: &StateVec) { + self.get_state_vector(); + let mut deltas = HashMap::new(); - for (user, strokes_state) in &self.get_state_vector().strokes { + web_sys::console::log_1(&format!("local {:?}", self.state).into()); + web_sys::console::log_1(&format!("remote {:?}", remote_state).into()); + + for (user, strokes_state) in &self.state.strokes { let strokes = &self.crdt.get(user).unwrap().strokes; if let Some(remote_strokes_state) = remote_state.strokes.get(user) { @@ -1118,14 +1420,12 @@ impl CRDT { } } else { for stroke in strokes { - let len = stroke.points.len(); - - if len == 0 { + if stroke.points.is_empty() { continue; }; let mut stroke_delta = StrokeDelta::new(); - stroke_delta.points.reserve(len); + stroke_delta.points.reserve(stroke.points.len()); for (point_idx, point) in stroke.points.deref() { stroke_delta.points.insert(point_idx, point.clone()); @@ -1136,8 +1436,43 @@ impl CRDT { } } + for (stroke_id, intervals_state) in &self.state.intervals { + let entry = self.crdt.get(&stroke_id.0).unwrap(); + let intervals = &entry + .strokes + .get(entry.get_stroke_idx(stroke_id.1).unwrap()) + .unwrap() + .intervals; + + if let Some(remote_intervals_state) = remote_state.intervals.get(stroke_id) { + if remote_intervals_state == intervals_state { + continue; + }; + + let remote_intervals: IntervalUnion<NotNan<f32>> = + ((*remote_intervals_state).clone()).into(); + + let mut difference = (*intervals).clone(); + difference.difference(&remote_intervals); + + let delta = deltas + .entry((*stroke_id).clone()) + .or_insert_with(StrokeDelta::new); + + delta.intervals = difference; + } else { + let delta = deltas + .entry((*stroke_id).clone()) + .or_insert_with(StrokeDelta::new); + + delta.intervals.0.extend(intervals.0.deref()); + } + } + + web_sys::console::log_1(&format!("deltas {:?}", deltas).into()); + self.event_listener.on_deltas_from_state(user, deltas) } - // TODO: intervals in StateVec + // TODO: implement serialisation/deserialisation for StateVec and DeltaVec (new type still needed) such that user UUIDs (incl. in stroke IDs) are encoded as (usize, u8) where the former is the index into a "VecSet" of canonical UUIDs and the latter is the modifier to the canonical ID } diff --git a/src/lib.rs b/src/lib.rs index e9ad5d2dd97d5ab64e92617f672e4be6f49699bd..235e23d892fc53abcf741b6b796f15f2ef02903e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ use wasm_bindgen::prelude::*; use js_sys::Error; +use ordered_float::NotNan; use serde_wasm_bindgen::to_value as to_js_value; use std::collections::HashMap; use std::option::Option; @@ -40,7 +41,7 @@ impl EventListener for WasmEventListener { self.on_stroke(stroke, to_js_value(points).unwrap()) } - fn on_interval(&self, stroke: String, intervals: &IntervalUnion<f32>) { + fn on_interval(&self, stroke: String, intervals: &IntervalUnion<NotNan<f32>>) { self.on_interval(stroke, to_js_value(intervals).unwrap()) } @@ -144,12 +145,12 @@ impl WasmCRDT { Ok(self.0.apply_deltas(deltas)) } - pub fn get_state_vector(&self) -> Box<[u8]> { + pub fn get_state_vector(&mut self) -> Box<[u8]> { packing::pack(&bincode::serialize(&self.0.get_state_vector()).unwrap()).into_boxed_slice() } pub fn fetch_deltas_from_state_vector( - &self, + &mut self, user: String, remote_state: Box<[u8]>, ) -> Result<(), JsValue> { diff --git a/www/index.js b/www/index.js index d18131d8dc2cfd6544ff1da9fc0a5ec3e01b967d..a28863e956348ed140681dfdb31b3850aa9376ce 100644 --- a/www/index.js +++ b/www/index.js @@ -29,6 +29,7 @@ crdt.add_stroke(4, 2, 3.14, "ffff00") crdt.add_stroke(4, 2, 3.14, "ffff00") let end_id = crdt.add_stroke(4, 2, 3.14, "ffff00") +crdt.erase_stroke(end_id, 0.0, 0.0) console.log("canonical stroke author 4:", WasmCRDT.canonicalise_stroke_author(end_id)) crdt.add_stroke(4, 2, 3.14, "ffff00")