yjs/tests/encoding.tests.js
2022-09-23 12:05:48 +02:00

468 lines
14 KiB
JavaScript

import * as t from 'lib0/testing'
import * as promise from 'lib0/promise'
import {
contentRefs,
readContentBinary,
readContentDeleted,
readContentString,
readContentJSON,
readContentEmbed,
readContentType,
readContentFormat,
readContentAny,
readContentDoc,
Doc,
PermanentUserData,
encodeStateAsUpdate,
applyUpdate
} from '../src/internals.js'
import * as Y from '../src/index.js'
/**
* @param {t.TestCase} tc
*/
export const testStructReferences = tc => {
t.assert(contentRefs.length === 11)
t.assert(contentRefs[1] === readContentDeleted)
t.assert(contentRefs[2] === readContentJSON) // TODO: deprecate content json?
t.assert(contentRefs[3] === readContentBinary)
t.assert(contentRefs[4] === readContentString)
t.assert(contentRefs[5] === readContentEmbed)
t.assert(contentRefs[6] === readContentFormat)
t.assert(contentRefs[7] === readContentType)
t.assert(contentRefs[8] === readContentAny)
t.assert(contentRefs[9] === readContentDoc)
// contentRefs[10] is reserved for Skip structs
}
/**
* There is some custom encoding/decoding happening in PermanentUserData.
* This is why it landed here.
*
* @param {t.TestCase} tc
*/
export const testPermanentUserData = async tc => {
const ydoc1 = new Doc()
const ydoc2 = new Doc()
const pd1 = new PermanentUserData(ydoc1)
const pd2 = new PermanentUserData(ydoc2)
pd1.setUserMapping(ydoc1, ydoc1.clientID, 'user a')
pd2.setUserMapping(ydoc2, ydoc2.clientID, 'user b')
ydoc1.getText().insert(0, 'xhi')
ydoc1.getText().delete(0, 1)
ydoc2.getText().insert(0, 'hxxi')
ydoc2.getText().delete(1, 2)
await promise.wait(10)
applyUpdate(ydoc2, encodeStateAsUpdate(ydoc1))
applyUpdate(ydoc1, encodeStateAsUpdate(ydoc2))
// now sync a third doc with same name as doc1 and then create PermanentUserData
const ydoc3 = new Doc()
applyUpdate(ydoc3, encodeStateAsUpdate(ydoc1))
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 {any}
*/
let sv = 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)
}
/** @function
* @param {number} x
*/
const splitClocksBy = (x) => {
/**
* @param {number} _client
* @param {number} clock
* @param {number} maxClock
*/
return function * (_client, clock, maxClock) {
while (clock < maxClock) {
clock = Math.min(clock + x, maxClock)
clock = yield clock
}
}
}
/**
* @param {t.TestCase} tc
*/
export const testEncodeStateAsUpdatesWithOneClient = tc => {
const yDoc = new Y.Doc()
const yText = yDoc.getText('textBlock')
yText.applyDelta([{ insert: 'r' }])
yText.applyDelta([{ insert: 'o' }])
yText.applyDelta([{ insert: 'n' }])
yText.applyDelta([{ insert: 'e' }])
yText.applyDelta([{ insert: 'n' }])
const updates = Array.from(Y.encodeStateAsStreamOfUpdates(yDoc, { clockSplits: splitClocksBy(1) }))
const yDocToAssert = new Y.Doc()
updates.forEach((update) => {
Y.applyUpdate(yDocToAssert, update)
})
t.compareStrings(yDocToAssert.getText('textBlock').toString(), 'nenor')
// yDoc did 5 updates
// 1 (empty) delete set
t.compare(6, updates.length)
}
/**
* @param {t.TestCase} tc
*/
export const testEncodeStateAsUpdatesWithTwoClients = tc => {
// Arrange
const yDoc = new Y.Doc()
const yText = yDoc.getText('textBlock')
yText.applyDelta([{ insert: 'r' }])
yText.applyDelta([{ insert: 'o' }])
yText.applyDelta([{ insert: 'n' }])
const remoteDoc = new Y.Doc()
Y.applyUpdate(remoteDoc, Y.encodeStateAsUpdate(yDoc))
remoteDoc.getText('textBlock').applyDelta([{ insert: 'e' }])
Y.applyUpdate(yDoc, Y.encodeStateAsUpdate(remoteDoc))
yText.applyDelta([{ insert: 'n' }])
// Act
const updates = Array.from(Y.encodeStateAsStreamOfUpdates(yDoc, { clockSplits: splitClocksBy(1) }))
// Assert
const yDocToAssert = new Y.Doc()
updates.forEach((update) => {
Y.applyUpdate(yDocToAssert, update)
})
t.compareStrings(yDocToAssert.getText('textBlock').toString(), 'nenor')
// yDoc did 3+1=4 updates
// remoteDoc did 1 update
// 1 (empty) delete set
t.compare(6, updates.length)
}
/**
* @param {t.TestCase} tc
*/
export const testEncodeStateAsUpdatesWithItemsOfLength2 = tc => {
// Arrange
const yDoc = new Y.Doc()
const yText = yDoc.getText('textBlock')
yText.applyDelta([{ insert: 'or' }])
yText.applyDelta([{ insert: 'n' }])
yText.applyDelta([{ insert: 'ne' }])
// Act
const updates = Array.from(Y.encodeStateAsStreamOfUpdates(yDoc, { clockSplits: splitClocksBy(1) }))
// Assert
// yDoc did 3 updates (ne will keep together, even if we use clockSplit of 1)
// 1 (empty) delete set
t.compare(3 + 1, updates.length)
const yDocToAssert = new Y.Doc()
Y.applyUpdate(yDocToAssert, updates[0]) // delete set
t.compareStrings(yDocToAssert.getText('textBlock').toString(), '')
t.compare(Y.getState(yDocToAssert.store, yDoc.clientID), 0)
Y.applyUpdate(yDocToAssert, updates[1]) // delete set
t.compareStrings(yDocToAssert.getText('textBlock').toString(), 'or')
t.compare(Y.getState(yDocToAssert.store, yDoc.clientID), 2)
Y.applyUpdate(yDocToAssert, updates[2]) // delete set
t.compareStrings(yDocToAssert.getText('textBlock').toString(), 'nor')
t.compare(Y.getState(yDocToAssert.store, yDoc.clientID), 3)
Y.applyUpdate(yDocToAssert, updates[3]) // delete set
t.compareStrings(yDocToAssert.getText('textBlock').toString(), 'nenor')
t.compare(Y.getState(yDocToAssert.store, yDoc.clientID), 5)
}
/**
* @param {t.TestCase} tc
*/
export const testEncodeStateAsUpdatesWithBadClockSplits = tc => {
const yDoc = new Y.Doc()
const yText = yDoc.getText('textBlock')
yText.applyDelta([{ insert: 'r' }])
yText.applyDelta([{ insert: 'o' }])
yText.applyDelta([{ insert: 'n' }])
yText.applyDelta([{ insert: 'e' }])
yText.applyDelta([{ insert: 'n' }])
const updates = Array.from(Y.encodeStateAsStreamOfUpdates(yDoc, {
clockSplits: function * (_client, clock, maxClock) {
clock = yield clock - 1
clock = yield clock + 1 // first message
clock = yield clock
clock = yield clock
clock = yield clock
clock = yield clock + 1 // second message
clock = yield maxClock + 100 // last message
}
}))
const yDocToAssert = new Y.Doc()
// Delete set message
Y.applyUpdate(yDocToAssert, updates[0])
t.compareStrings(yDocToAssert.getText('textBlock').toString(), '')
// first message
Y.applyUpdate(yDocToAssert, updates[1])
t.compareStrings(yDocToAssert.getText('textBlock').toString(), 'r')
// second message
Y.applyUpdate(yDocToAssert, updates[2])
t.compareStrings(yDocToAssert.getText('textBlock').toString(), 'or')
Y.applyUpdate(yDocToAssert, updates[3])
t.compareStrings(yDocToAssert.getText('textBlock').toString(), 'nenor')
t.compare(4, updates.length)
}
/**
* @param {t.TestCase} tc
*/
export const testEncodeStateAsUpdatesShouldRespectClockSplits = tc => {
// Arrange
const yDoc = new Y.Doc()
/**
* @type {Array<number>}
*/
const clockSplits = []
/**
* @type {Array<Uint8Array>}
*/
const expectedUpdates = []
yDoc.on('update', (update) => {
clockSplits.push(Y.getState(yDoc.store, yDoc.clientID))
expectedUpdates.push(update)
})
const cells = yDoc.getArray('cells')
const cell0 = new Y.Map()
cell0.set('id', new Y.Text('zero'))
cell0.set('source', new Y.Text('# Hello World'))
cells.push([cell0])
const cell1 = new Y.Map()
cell1.set('id', new Y.Text('one'))
cell1.set('source', new Y.Text('import pandas as pd'))
cells.push([cell1])
yDoc.transact(() => {
yDoc.getMap('meta').set('language', 'python')
yDoc.getMap('state').set('version', 3)
})
// Act
const streamOfUpdates = Y.encodeStateAsStreamOfUpdates(yDoc, {
clockSplits: () => clockSplits
})
// Assert
const yDocToAssert = new Y.Doc()
let i = -1
for (const update of streamOfUpdates) {
Y.applyUpdate(yDocToAssert, update)
if (i >= 0) { // i == -1 is the delete set message
t.compare(update, expectedUpdates[i], 'updates match')
t.compare(Y.getState(yDocToAssert.store, yDoc.clientID), clockSplits[i], 'correct clock afterwards')
}
i++
}
t.compare(yDocToAssert.getArray('cells').toJSON(), [
{ id: 'zero', source: '# Hello World' },
{ id: 'one', source: 'import pandas as pd' }
])
t.compare(yDocToAssert.getMap('meta').toJSON(), { language: 'python' })
t.compare(yDocToAssert.getMap('state').toJSON(), { version: 3 })
}
/**
* @param {t.TestCase} tc
*/
export const testEncodeStateAsUpdatesWithMaps = tc => {
// Arrange
const yDoc = new Y.Doc()
const yMap = yDoc.getMap('myMap')
yMap.set('foo', 'foo1')
yMap.set('bar', 'bar1')
yMap.set('quux', 'quux1')
yMap.set('bar', 'bar2')
const expectedMap = {
foo: 'foo1',
bar: 'bar2',
quux: 'quux1'
}
// Act
const updates = Array.from(Y.encodeStateAsStreamOfUpdates(yDoc, {
clockSplits: splitClocksBy(2)
}))
// Assert
t.compare(3, updates.length)
const yDocToAssert = new Y.Doc()
// Delete set message
Y.applyUpdate(yDocToAssert, updates[0])
t.compare(10, updates[0].length) // There is a delete set!
t.compare(0, updates[0][0]) // No updates by clients
t.compareObjects(yDocToAssert.getMap('myMap').toJSON(), {}, 'after update 1')
// First 2 updates
Y.applyUpdate(yDocToAssert, updates[1])
// bar is not here because the item is in the delete set
t.compareObjects(yDocToAssert.getMap('myMap').toJSON(), { foo: 'foo1' }, 'after update 2')
// Last 2 updates
Y.applyUpdate(yDocToAssert, updates[2])
t.compareObjects(yDocToAssert.getMap('myMap').toJSON(), { foo: 'foo1', bar: 'bar2', quux: 'quux1' }, 'after update 3')
t.compareObjects(yDocToAssert.getMap('myMap').toJSON(), expectedMap)
}
/**
* @param {t.TestCase} tc
*/
export const testEncodeStateAsUpdatesWithDifferentSortingAndEditsByClients = tc => {
// Arrange
const yNotebook = new Y.Doc()
/**
* @type {Array<number>}
*/
const clockSplits = []
yNotebook.on('update', (update) => {
clockSplits.push(Y.getState(yNotebook.store, yNotebook.clientID))
})
const cells = yNotebook.getArray('cells')
const cell0 = new Y.Map()
cell0.set('id', new Y.Text('zero'))
cell0.set('source', new Y.Text('# Hello World'))
cells.push([cell0])
const cell1 = new Y.Map()
cell1.set('id', new Y.Text('one'))
cell1.set('source', new Y.Text('import pandas as pd'))
cells.push([cell1])
const cell2 = new Y.Map()
cell2.set('id', new Y.Text('two'))
cell2.set('source', new Y.Text('# Conclusion'))
cells.push([cell2])
yNotebook.transact(() => {
yNotebook.getMap('meta').set('language', 'python')
yNotebook.getMap('state').set('version', 3)
})
const clientDoc = new Y.Doc()
Y.applyUpdate(clientDoc, Y.encodeStateAsUpdate(yNotebook))
const source = clientDoc.getArray('cells').get(1).get('source')
source.insert(source.length, '\nimport random')
t.compare(source.toString(), 'import pandas as pd\nimport random', 'clientDoc should have right code')
Y.applyUpdate(yNotebook, Y.encodeStateAsUpdate(clientDoc))
console.log('clockSplits', clockSplits, yNotebook.clientID)
const updates = Array.from(Y.encodeStateAsStreamOfUpdates(yNotebook, {
clockSplits: (client) => {
if (client === yNotebook.clientID) {
return clockSplits
}
return []
},
sortClients: clientClocks => {
return [
...clientClocks.filter(([client]) => client === clientDoc.clientID),
...clientClocks.filter(([client]) => client === yNotebook.clientID)
]
}
}))
const ydoc = new Y.Doc()
Y.applyUpdate(ydoc, updates[0]) // delete set
t.compare(ydoc.getArray('cells').toJSON(), [])
Y.applyUpdate(ydoc, updates[1]) // clientDoc updates
t.compare(ydoc.getArray('cells').toJSON(), [])
Y.applyUpdate(ydoc, updates[2]) // cell 0 initialized
t.compare(ydoc.getArray('cells').toJSON(), [{ id: 'zero', source: '# Hello World' }])
Y.applyUpdate(ydoc, updates[3]) // cell 1 initialized, immediately applies edits by clients
t.compare(ydoc.getArray('cells').toJSON(), [
{ id: 'zero', source: '# Hello World' },
{ id: 'one', source: 'import pandas as pd\nimport random' }
])
Y.applyUpdate(ydoc, updates[4]) // cell 2 initialized
t.compare(ydoc.getArray('cells').toJSON(), [
{ id: 'zero', source: '# Hello World' },
{ id: 'one', source: 'import pandas as pd\nimport random' },
{ id: 'two', source: '# Conclusion' }
])
Y.applyUpdate(ydoc, updates[5]) // metadata
t.compare(ydoc.getArray('cells').toJSON(), [
{ id: 'zero', source: '# Hello World' },
{ id: 'one', source: 'import pandas as pd\nimport random' },
{ id: 'two', source: '# Conclusion' }
])
t.compare(6, updates.length)
}