From 5cac153a1702d3ba3a959a3f15f111ff61f033ad Mon Sep 17 00:00:00 2001
From: Kevin Jahns <kevin.jahns@pm.me>
Date: Mon, 7 Jun 2021 19:41:54 +0200
Subject: [PATCH] Fix #308 - stateVector should ignore skips and incomplete
 content

---
 src/utils/updates.js    | 36 +++++++++++++++++++--------------
 tests/encoding.tests.js | 44 +++++++++++++++++++++++++++++++++++++++++
 tests/updates.tests.js  |  2 --
 3 files changed, 65 insertions(+), 17 deletions(-)

diff --git a/src/utils/updates.js b/src/utils/updates.js
index b3aec8d6..6b54d83d 100644
--- a/src/utils/updates.js
+++ b/src/utils/updates.js
@@ -149,33 +149,39 @@ export const mergeUpdates = updates => mergeUpdatesV2(updates, UpdateDecoderV1,
  */
 export const encodeStateVectorFromUpdateV2 = (update, YEncoder = DSEncoderV2, YDecoder = UpdateDecoderV2) => {
   const encoder = new YEncoder()
-  const updateDecoder = new LazyStructReader(new YDecoder(decoding.createDecoder(update)), true)
+  const updateDecoder = new LazyStructReader(new YDecoder(decoding.createDecoder(update)), false)
   let curr = updateDecoder.curr
   if (curr !== null) {
-    let size = 1
+    let size = 0
     let currClient = curr.id.client
-    let currClock = curr.id.clock
-    let stopCounting = false
+    let currClock = 0
+    let stopCounting = curr.id.clock !== 0 // must start at 0
     for (; curr !== null; curr = updateDecoder.next()) {
-      if (currClient !== curr.id.client) {
-        size++
-        // We found a new client
-        // write what we have to the encoder
-        encoding.writeVarUint(encoder.restEncoder, currClient)
-        encoding.writeVarUint(encoder.restEncoder, currClock)
-        currClient = curr.id.client
-        stopCounting = false
-      }
+      // we ignore skips
       if (curr.constructor === Skip) {
         stopCounting = true
       }
       if (!stopCounting) {
         currClock = curr.id.clock + curr.length
       }
+      if (currClient !== curr.id.client) {
+        if (currClock !== 0) {
+          size++
+          // We found a new client
+          // write what we have to the encoder
+          encoding.writeVarUint(encoder.restEncoder, currClient)
+          encoding.writeVarUint(encoder.restEncoder, currClock)
+        }
+        currClient = curr.id.client
+        stopCounting = false
+      }
     }
     // write what we have
-    encoding.writeVarUint(encoder.restEncoder, currClient)
-    encoding.writeVarUint(encoder.restEncoder, currClock)
+    if (currClock !== 0) {
+      size++
+      encoding.writeVarUint(encoder.restEncoder, currClient)
+      encoding.writeVarUint(encoder.restEncoder, currClock)
+    }
     // prepend the size of the state vector
     const enc = encoding.createEncoder()
     encoding.writeVarUint(enc, size)
diff --git a/tests/encoding.tests.js b/tests/encoding.tests.js
index 207f13d5..6a191009 100644
--- a/tests/encoding.tests.js
+++ b/tests/encoding.tests.js
@@ -18,6 +18,8 @@ import {
   applyUpdate
 } from '../src/internals.js'
 
+import * as Y from '../src/index.js'
+
 /**
  * @param {t.TestCase} tc
  */
@@ -62,3 +64,45 @@ export const testPermanentUserData = async tc => {
   const pd3 = new PermanentUserData(ydoc3)
   pd3.setUserMapping(ydoc3, ydoc3.clientID, 'user a')
 }
+
+/**
+ * Reported here: https://github.com/yjs/yjs/issues/308
+ * @param {t.TestCase} tc
+ */
+export const testDiffStateVectorOfUpdateIsEmpty = tc => {
+  const ydoc = new Y.Doc()
+  /**
+   * @type {null | Uint8Array}
+   */
+  let sv = /* any */ (null)
+  ydoc.getText().insert(0, 'a')
+  ydoc.on('update', update => {
+    sv = Y.encodeStateVectorFromUpdate(update)
+  })
+  // should produce an update with an empty state vector (because previous ops are missing)
+  ydoc.getText().insert(0, 'a')
+  t.assert(sv !== null && sv.byteLength === 1 && sv[0] === 0)
+}
+
+/**
+ * Reported here: https://github.com/yjs/yjs/issues/308
+ * @param {t.TestCase} tc
+ */
+export const testDiffStateVectorOfUpdateIgnoresSkips = tc => {
+  const ydoc = new Y.Doc()
+  /**
+   * @type {Array<Uint8Array>}
+   */
+  const updates = []
+  ydoc.on('update', update => {
+    updates.push(update)
+  })
+  ydoc.getText().insert(0, 'a')
+  ydoc.getText().insert(0, 'b')
+  ydoc.getText().insert(0, 'c')
+  const update13 = Y.mergeUpdates([updates[0], updates[2]])
+  const sv = Y.encodeStateVectorFromUpdate(update13)
+  const state = Y.decodeStateVector(sv)
+  t.assert(state.get(ydoc.clientID) === 1)
+  t.assert(state.size === 1)
+}
diff --git a/tests/updates.tests.js b/tests/updates.tests.js
index d9f8743d..924dd220 100644
--- a/tests/updates.tests.js
+++ b/tests/updates.tests.js
@@ -166,9 +166,7 @@ const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
         const targetSV = Y.encodeStateVectorFromUpdateV2(Y.mergeUpdatesV2(updates.slice(0, j)))
         const diffed = enc.diffUpdate(mergedUpdates, targetSV)
         const diffedMeta = enc.parseUpdateMeta(diffed)
-        const decDiffedSV = Y.decodeStateVector(enc.encodeStateVectorFromUpdate(diffed))
         t.compare(partMeta, diffedMeta)
-        t.compare(decDiffedSV, partMeta.to)
         {
           // We can'd do the following
           //  - t.compare(diffed, mergedDeletes)