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")