From fbbf085278e842c9f5c173f7f591b2d2f3863465 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Thu, 17 Dec 2020 21:50:39 +0100 Subject: [PATCH] add mergeUpdates tests to comparison framework --- src/structs/Item.js | 13 ++++++--- src/utils/Snapshot.js | 4 +-- src/utils/Transaction.js | 5 ++-- src/utils/encoding.js | 29 ++++--------------- tests/testHelper.js | 61 +++++++++++++++++++++++++++++++++++++--- 5 files changed, 76 insertions(+), 36 deletions(-) diff --git a/src/structs/Item.js b/src/structs/Item.js index c63dff84..adc64023 100644 --- a/src/structs/Item.js +++ b/src/structs/Item.js @@ -640,10 +640,7 @@ export class Item extends AbstractStruct { } if (origin === null && rightOrigin === null) { const parent = /** @type {AbstractType} */ (this.parent) - if (parent.constructor === String) { // this edge case was added by differential updates - encoder.writeParentInfo(true) // write parentYKey - encoder.writeString(parent) - } else { + if (parent._item !== undefined) { const parentItem = parent._item if (parentItem === null) { // parent type on y._map @@ -655,6 +652,14 @@ export class Item extends AbstractStruct { encoder.writeParentInfo(false) // write parent id encoder.writeLeftID(parentItem.id) } + } else if (parent.constructor === String) { // this edge case was added by differential updates + encoder.writeParentInfo(true) // write parentYKey + encoder.writeString(parent) + } else if (parent.constructor === ID) { + encoder.writeParentInfo(false) // write parent id + encoder.writeLeftID(parent) + } else { + error.unexpectedCase() } if (parentSub !== null) { encoder.writeString(parentSub) diff --git a/src/utils/Snapshot.js b/src/utils/Snapshot.js index ccd923e8..263bfa23 100644 --- a/src/utils/Snapshot.js +++ b/src/utils/Snapshot.js @@ -14,7 +14,6 @@ import { getState, findIndexSS, UpdateEncoderV2, - DefaultDSEncoder, applyUpdateV2, AbstractDSDecoder, AbstractDSEncoder, DSEncoderV2, DSDecoderV1, DSDecoderV2, Transaction, Doc, DeleteSet, Item // eslint-disable-line } from '../internals.js' @@ -23,6 +22,7 @@ import * as map from 'lib0/map.js' import * as set from 'lib0/set.js' import * as decoding from 'lib0/decoding.js' import * as encoding from 'lib0/encoding.js' +import { DSEncoderV1 } from './UpdateEncoder.js' export class Snapshot { /** @@ -91,7 +91,7 @@ export const encodeSnapshotV2 = (snapshot, encoder = new DSEncoderV2()) => { * @param {Snapshot} snapshot * @return {Uint8Array} */ -export const encodeSnapshot = snapshot => encodeSnapshotV2(snapshot, new DefaultDSEncoder()) +export const encodeSnapshot = snapshot => encodeSnapshotV2(snapshot, new DSEncoderV1()) /** * @param {Uint8Array} buf diff --git a/src/utils/Transaction.js b/src/utils/Transaction.js index 4a1746ac..929d061b 100644 --- a/src/utils/Transaction.js +++ b/src/utils/Transaction.js @@ -11,7 +11,7 @@ import { Item, generateNewClientId, createID, - AbstractUpdateEncoder, GC, StructStore, UpdateEncoderV2, DefaultUpdateEncoder, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line + AbstractUpdateEncoder, GC, StructStore, UpdateEncoderV2, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line } from '../internals.js' import * as map from 'lib0/map.js' @@ -19,6 +19,7 @@ import * as math from 'lib0/math.js' import * as set from 'lib0/set.js' import * as logging from 'lib0/logging.js' import { callAll } from 'lib0/function.js' +import { UpdateEncoderV1 } from './UpdateEncoder.js' /** * A transaction is created for every change on the Yjs model. It is possible @@ -337,7 +338,7 @@ const cleanupTransactions = (transactionCleanups, i) => { // @todo Merge all the transactions into one and provide send the data as a single update message doc.emit('afterTransactionCleanup', [transaction, doc]) if (doc._observers.has('update')) { - const encoder = new DefaultUpdateEncoder() + const encoder = new UpdateEncoderV1() const hasContent = writeUpdateMessageFromTransaction(encoder, transaction) if (hasContent) { doc.emit('update', [encoder.toUint8Array(), transaction.origin, doc]) diff --git a/src/utils/encoding.js b/src/utils/encoding.js index 9efd16ee..56cb6836 100644 --- a/src/utils/encoding.js +++ b/src/utils/encoding.js @@ -41,25 +41,6 @@ import * as decoding from 'lib0/decoding.js' import * as binary from 'lib0/binary.js' import * as map from 'lib0/map.js' -export let DefaultDSEncoder = DSEncoderV1 -export let DefaultDSDecoder = DSDecoderV1 -export let DefaultUpdateEncoder = UpdateEncoderV1 -export let DefaultUpdateDecoder = UpdateDecoderV1 - -export const useV1Encoding = () => { - DefaultDSEncoder = DSEncoderV1 - DefaultDSDecoder = DSDecoderV1 - DefaultUpdateEncoder = UpdateEncoderV1 - DefaultUpdateDecoder = UpdateDecoderV1 -} - -export const useV2Encoding = () => { - DefaultDSEncoder = DSEncoderV2 - DefaultDSDecoder = DSDecoderV2 - DefaultUpdateEncoder = UpdateEncoderV2 - DefaultUpdateDecoder = UpdateDecoderV2 -} - /** * @param {AbstractUpdateEncoder} encoder * @param {Array} structs All structs by `client` @@ -445,7 +426,7 @@ export const readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = n * * @function */ -export const readUpdate = (decoder, ydoc, transactionOrigin) => readUpdateV2(decoder, ydoc, transactionOrigin, new DefaultUpdateDecoder(decoder)) +export const readUpdate = (decoder, ydoc, transactionOrigin) => readUpdateV2(decoder, ydoc, transactionOrigin, new UpdateDecoderV1(decoder)) /** * Apply a document update created by, for example, `y.on('update', update => ..)` or `update = encodeStateAsUpdate()`. @@ -475,7 +456,7 @@ export const applyUpdateV2 = (ydoc, update, transactionOrigin, YDecoder = Update * * @function */ -export const applyUpdate = (ydoc, update, transactionOrigin) => applyUpdateV2(ydoc, update, transactionOrigin, DefaultUpdateDecoder) +export const applyUpdate = (ydoc, update, transactionOrigin) => applyUpdateV2(ydoc, update, transactionOrigin, UpdateDecoderV1) /** * Write all the document as a single update message. If you specify the state of the remote client (`targetStateVector`) it will @@ -523,7 +504,7 @@ export const encodeStateAsUpdateV2 = (doc, encodedTargetStateVector, encoder = n * * @function */ -export const encodeStateAsUpdate = (doc, encodedTargetStateVector) => encodeStateAsUpdateV2(doc, encodedTargetStateVector, new DefaultUpdateEncoder()) +export const encodeStateAsUpdate = (doc, encodedTargetStateVector) => encodeStateAsUpdateV2(doc, encodedTargetStateVector, new UpdateEncoderV1()) /** * Read state vector from Decoder and return as Map @@ -562,7 +543,7 @@ export const decodeStateVectorV2 = decodedState => readStateVector(new DSDecoder * * @function */ -export const decodeStateVector = decodedState => readStateVector(new DefaultDSDecoder(decoding.createDecoder(decodedState))) +export const decodeStateVector = decodedState => readStateVector(new DSDecoderV1(decoding.createDecoder(decodedState))) /** * @param {AbstractDSEncoder} encoder @@ -608,4 +589,4 @@ export const encodeStateVectorV2 = (doc, encoder = new DSEncoderV2()) => { * * @function */ -export const encodeStateVector = doc => encodeStateVectorV2(doc, new DefaultDSEncoder()) +export const encodeStateVector = doc => encodeStateVectorV2(doc, new DSEncoderV1()) diff --git a/tests/testHelper.js b/tests/testHelper.js index 2409af79..e7186268 100644 --- a/tests/testHelper.js +++ b/tests/testHelper.js @@ -27,6 +27,33 @@ const broadcastMessage = (y, m) => { } } +let useV2 = false + +export let encodeStateAsUpdate = Y.encodeStateAsUpdate +export let mergeUpdates = Y.mergeUpdates +export let applyUpdate = Y.applyUpdate +export let logUpdate = Y.logUpdate +export let updateEventName = 'update' + +const setEncoders = () => { + encodeStateAsUpdate = useV2 ? Y.encodeStateAsUpdateV2 : Y.encodeStateAsUpdate + mergeUpdates = useV2 ? Y.mergeUpdatesV2 : Y.mergeUpdates + applyUpdate = useV2 ? Y.applyUpdateV2 : Y.applyUpdate + logUpdate = useV2 ? Y.logUpdateV2 : Y.logUpdate + updateEventName = useV2 ? 'updateV2' : 'update' +} + +const useV1Encoding = () => { + useV2 = false + setEncoders() +} + +const useV2Encoding = () => { + useV2 = false + console.error('sync protocol doesnt support v2 protocol yet, fallback to v1 encoding') // @Todo + setEncoders() +} + export class TestYInstance extends Y.Doc { /** * @param {TestConnector} testConnector @@ -44,12 +71,19 @@ export class TestYInstance extends Y.Doc { */ this.receiving = new Map() testConnector.allConns.add(this) + /** + * The list of received updates. + * We are going to merge them later using Y.mergeUpdates and check if the resulting document is correct. + * @type {Array} + */ + this.updates = [] // set up observe on local model - this.on('update', /** @param {Uint8Array} update @param {any} origin */ (update, origin) => { + this.on(updateEventName, /** @param {Uint8Array} update @param {any} origin */ (update, origin) => { if (origin !== testConnector) { const encoder = encoding.createEncoder() syncProtocol.writeUpdate(encoder, update) broadcastMessage(this, encoding.toUint8Array(encoder)) + this.updates.push(update) } }) this.connect() @@ -162,6 +196,17 @@ export class TestConnector { // send reply message sender._receive(encoding.toUint8Array(encoder), receiver) } + { + // If update message, add the received message to the list of received messages + const decoder = decoding.createDecoder(m) + const messageType = decoding.readVarUint(decoder) + switch (messageType) { + case syncProtocol.messageYjsUpdate: + case syncProtocol.messageYjsSyncStep2: + receiver.updates.push(decoding.readVarUint8Array(decoder)) + break + } + } return true } return false @@ -240,9 +285,9 @@ export const init = (tc, { users = 5 } = {}, initTestObject) => { const gen = tc.prng // choose an encoding approach at random if (prng.bool(gen)) { - Y.useV2Encoding() + useV2Encoding() } else { - Y.useV1Encoding() + useV1Encoding() } const testConnector = new TestConnector(gen) @@ -258,7 +303,7 @@ export const init = (tc, { users = 5 } = {}, initTestObject) => { } testConnector.syncAll() result.testObjects = result.users.map(initTestObject || (() => null)) - Y.useV1Encoding() + useV1Encoding() return /** @type {any} */ (result) } @@ -274,6 +319,14 @@ export const init = (tc, { users = 5 } = {}, initTestObject) => { export const compare = users => { users.forEach(u => u.connect()) while (users[0].tc.flushAllMessages()) {} + // For each document, merge all received document updates with Y.mergeUpdates and create a new document which will be added to the list of "users" + // This ensures that mergeUpdates works correctly + const mergedDocs = users.map(user => { + const ydoc = new Y.Doc() + applyUpdate(ydoc, mergeUpdates(user.updates)) + return ydoc + }) + users.push(.../** @type {any} */(mergedDocs)) const userArrayValues = users.map(u => u.getArray('array').toJSON()) const userMapValues = users.map(u => u.getMap('map').toJSON()) const userXmlValues = users.map(u => u.get('xml', Y.YXmlElement).toString())