implement diffUpdates with tests - #263
This commit is contained in:
		
							parent
							
								
									7edbb2485f
								
							
						
					
					
						commit
						275d52b19d
					
				@ -57,14 +57,12 @@ export {
 | 
				
			|||||||
  encodeStateAsUpdate,
 | 
					  encodeStateAsUpdate,
 | 
				
			||||||
  encodeStateAsUpdateV2,
 | 
					  encodeStateAsUpdateV2,
 | 
				
			||||||
  encodeStateVector,
 | 
					  encodeStateVector,
 | 
				
			||||||
  encodeStateVectorV2,
 | 
					 | 
				
			||||||
  UndoManager,
 | 
					  UndoManager,
 | 
				
			||||||
  decodeSnapshot,
 | 
					  decodeSnapshot,
 | 
				
			||||||
  encodeSnapshot,
 | 
					  encodeSnapshot,
 | 
				
			||||||
  decodeSnapshotV2,
 | 
					  decodeSnapshotV2,
 | 
				
			||||||
  encodeSnapshotV2,
 | 
					  encodeSnapshotV2,
 | 
				
			||||||
  decodeStateVector,
 | 
					  decodeStateVector,
 | 
				
			||||||
  decodeStateVectorV2,
 | 
					 | 
				
			||||||
  logUpdate,
 | 
					  logUpdate,
 | 
				
			||||||
  logUpdateV2,
 | 
					  logUpdateV2,
 | 
				
			||||||
  isDeleted,
 | 
					  isDeleted,
 | 
				
			||||||
@ -82,5 +80,7 @@ export {
 | 
				
			|||||||
  encodeStateVectorFromUpdate,
 | 
					  encodeStateVectorFromUpdate,
 | 
				
			||||||
  encodeStateVectorFromUpdateV2,
 | 
					  encodeStateVectorFromUpdateV2,
 | 
				
			||||||
  encodeRelativePosition,
 | 
					  encodeRelativePosition,
 | 
				
			||||||
  decodeRelativePosition
 | 
					  decodeRelativePosition,
 | 
				
			||||||
 | 
					  diffUpdate,
 | 
				
			||||||
 | 
					  diffUpdateV2
 | 
				
			||||||
} from './internals.js'
 | 
					} from './internals.js'
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@ import {
 | 
				
			|||||||
  UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, ID // eslint-disable-line
 | 
					  UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, ID // eslint-disable-line
 | 
				
			||||||
} from '../internals.js'
 | 
					} from '../internals.js'
 | 
				
			||||||
import * as error from 'lib0/error.js'
 | 
					import * as error from 'lib0/error.js'
 | 
				
			||||||
 | 
					import * as encoding from 'lib0/encoding.js'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const structSkipRefNumber = 10
 | 
					export const structSkipRefNumber = 10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -44,7 +45,8 @@ export class Skip extends AbstractStruct {
 | 
				
			|||||||
   */
 | 
					   */
 | 
				
			||||||
  write (encoder, offset) {
 | 
					  write (encoder, offset) {
 | 
				
			||||||
    encoder.writeInfo(structSkipRefNumber)
 | 
					    encoder.writeInfo(structSkipRefNumber)
 | 
				
			||||||
    encoder.writeLen(this.length - offset)
 | 
					    // write as VarUint because Skips can't make use of predictable length-encoding
 | 
				
			||||||
 | 
					    encoding.writeVarUint(encoder.restEncoder, this.length - offset)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
 | 
				
			|||||||
@ -29,14 +29,13 @@ import {
 | 
				
			|||||||
  UpdateDecoderV2,
 | 
					  UpdateDecoderV2,
 | 
				
			||||||
  UpdateEncoderV1,
 | 
					  UpdateEncoderV1,
 | 
				
			||||||
  UpdateEncoderV2,
 | 
					  UpdateEncoderV2,
 | 
				
			||||||
  DSDecoderV2,
 | 
					 | 
				
			||||||
  DSEncoderV2,
 | 
					  DSEncoderV2,
 | 
				
			||||||
  DSDecoderV1,
 | 
					  DSDecoderV1,
 | 
				
			||||||
  DSEncoderV1,
 | 
					  DSEncoderV1,
 | 
				
			||||||
  mergeUpdatesV2,
 | 
					  mergeUpdatesV2,
 | 
				
			||||||
  Skip,
 | 
					  Skip,
 | 
				
			||||||
  diffUpdate,
 | 
					  diffUpdateV2,
 | 
				
			||||||
  Doc, Transaction, GC, Item, StructStore // eslint-disable-line
 | 
					  DSDecoderV2, Doc, Transaction, GC, Item, StructStore // eslint-disable-line
 | 
				
			||||||
} from '../internals.js'
 | 
					} from '../internals.js'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import * as encoding from 'lib0/encoding.js'
 | 
					import * as encoding from 'lib0/encoding.js'
 | 
				
			||||||
@ -136,7 +135,7 @@ export const readClientsStructRefs = (decoder, doc) => {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        case 10: { // Skip Struct (nothing to apply)
 | 
					        case 10: { // Skip Struct (nothing to apply)
 | 
				
			||||||
          // @todo we could reduce the amount of checks by adding Skip struct to clientRefs so we know that something is missing.
 | 
					          // @todo we could reduce the amount of checks by adding Skip struct to clientRefs so we know that something is missing.
 | 
				
			||||||
          const len = decoder.readLen()
 | 
					          const len = decoding.readVarUint(decoder.restDecoder)
 | 
				
			||||||
          refs[i] = new Skip(createID(client, clock), len)
 | 
					          refs[i] = new Skip(createID(client, clock), len)
 | 
				
			||||||
          clock += len
 | 
					          clock += len
 | 
				
			||||||
          break
 | 
					          break
 | 
				
			||||||
@ -517,8 +516,8 @@ export const writeStateAsUpdate = (encoder, doc, targetStateVector = new Map())
 | 
				
			|||||||
 *
 | 
					 *
 | 
				
			||||||
 * @function
 | 
					 * @function
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export const encodeStateAsUpdateV2 = (doc, encodedTargetStateVector, encoder = new UpdateEncoderV2()) => {
 | 
					export const encodeStateAsUpdateV2 = (doc, encodedTargetStateVector = new Uint8Array([0]), encoder = new UpdateEncoderV2()) => {
 | 
				
			||||||
  const targetStateVector = encodedTargetStateVector == null ? new Map() : decodeStateVector(encodedTargetStateVector)
 | 
					  const targetStateVector = decodeStateVector(encodedTargetStateVector)
 | 
				
			||||||
  writeStateAsUpdate(encoder, doc, targetStateVector)
 | 
					  writeStateAsUpdate(encoder, doc, targetStateVector)
 | 
				
			||||||
  const updates = [encoder.toUint8Array()]
 | 
					  const updates = [encoder.toUint8Array()]
 | 
				
			||||||
  // also add the pending updates (if there are any)
 | 
					  // also add the pending updates (if there are any)
 | 
				
			||||||
@ -528,7 +527,7 @@ export const encodeStateAsUpdateV2 = (doc, encodedTargetStateVector, encoder = n
 | 
				
			|||||||
      updates.push(doc.store.pendingDs)
 | 
					      updates.push(doc.store.pendingDs)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (doc.store.pendingStructs) {
 | 
					    if (doc.store.pendingStructs) {
 | 
				
			||||||
      updates.push(diffUpdate(doc.store.pendingStructs.update, encodedTargetStateVector))
 | 
					      updates.push(diffUpdateV2(doc.store.pendingStructs.update, encodedTargetStateVector))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (updates.length > 1) {
 | 
					    if (updates.length > 1) {
 | 
				
			||||||
      return mergeUpdatesV2(updates)
 | 
					      return mergeUpdatesV2(updates)
 | 
				
			||||||
@ -578,7 +577,7 @@ export const readStateVector = decoder => {
 | 
				
			|||||||
 *
 | 
					 *
 | 
				
			||||||
 * @function
 | 
					 * @function
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export const decodeStateVectorV2 = decodedState => readStateVector(new DSDecoderV2(decoding.createDecoder(decodedState)))
 | 
					// export const decodeStateVectorV2 = decodedState => readStateVector(new DSDecoderV2(decoding.createDecoder(decodedState)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Read decodedState and return State as Map.
 | 
					 * Read decodedState and return State as Map.
 | 
				
			||||||
@ -615,21 +614,25 @@ export const writeDocumentStateVector = (encoder, doc) => writeStateVector(encod
 | 
				
			|||||||
/**
 | 
					/**
 | 
				
			||||||
 * Encode State as Uint8Array.
 | 
					 * Encode State as Uint8Array.
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * @param {Doc} doc
 | 
					 * @param {Doc|Map<number,number>} doc
 | 
				
			||||||
 * @param {DSEncoderV1 | DSEncoderV2} [encoder]
 | 
					 * @param {DSEncoderV1 | DSEncoderV2} [encoder]
 | 
				
			||||||
 * @return {Uint8Array}
 | 
					 * @return {Uint8Array}
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * @function
 | 
					 * @function
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export const encodeStateVectorV2 = (doc, encoder = new DSEncoderV2()) => {
 | 
					export const encodeStateVectorV2 = (doc, encoder = new DSEncoderV2()) => {
 | 
				
			||||||
 | 
					  if (doc instanceof Map) {
 | 
				
			||||||
 | 
					    writeStateVector(encoder, doc)
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
    writeDocumentStateVector(encoder, doc)
 | 
					    writeDocumentStateVector(encoder, doc)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  return encoder.toUint8Array()
 | 
					  return encoder.toUint8Array()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Encode State as Uint8Array.
 | 
					 * Encode State as Uint8Array.
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * @param {Doc} doc
 | 
					 * @param {Doc|Map<number,number>} doc
 | 
				
			||||||
 * @return {Uint8Array}
 | 
					 * @return {Uint8Array}
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * @function
 | 
					 * @function
 | 
				
			||||||
 | 
				
			|||||||
@ -3,6 +3,7 @@ import * as binary from 'lib0/binary.js'
 | 
				
			|||||||
import * as decoding from 'lib0/decoding.js'
 | 
					import * as decoding from 'lib0/decoding.js'
 | 
				
			||||||
import * as encoding from 'lib0/encoding.js'
 | 
					import * as encoding from 'lib0/encoding.js'
 | 
				
			||||||
import * as logging from 'lib0/logging.js'
 | 
					import * as logging from 'lib0/logging.js'
 | 
				
			||||||
 | 
					import * as math from 'lib0/math.js'
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  createID,
 | 
					  createID,
 | 
				
			||||||
  readItemContent,
 | 
					  readItemContent,
 | 
				
			||||||
@ -12,6 +13,7 @@ import {
 | 
				
			|||||||
  mergeDeleteSets,
 | 
					  mergeDeleteSets,
 | 
				
			||||||
  DSEncoderV1,
 | 
					  DSEncoderV1,
 | 
				
			||||||
  DSEncoderV2,
 | 
					  DSEncoderV2,
 | 
				
			||||||
 | 
					  decodeStateVector,
 | 
				
			||||||
  Item, GC, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2 // eslint-disable-line
 | 
					  Item, GC, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2 // eslint-disable-line
 | 
				
			||||||
} from '../internals.js'
 | 
					} from '../internals.js'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -28,7 +30,7 @@ function * lazyStructReaderGenerator (decoder) {
 | 
				
			|||||||
      const info = decoder.readInfo()
 | 
					      const info = decoder.readInfo()
 | 
				
			||||||
      // @todo use switch instead of ifs
 | 
					      // @todo use switch instead of ifs
 | 
				
			||||||
      if (info === 10) {
 | 
					      if (info === 10) {
 | 
				
			||||||
        const len = decoder.readLen()
 | 
					        const len = decoding.readVarUint(decoder.restDecoder)
 | 
				
			||||||
        yield new Skip(createID(client, clock), len)
 | 
					        yield new Skip(createID(client, clock), len)
 | 
				
			||||||
        clock += len
 | 
					        clock += len
 | 
				
			||||||
      } else if ((binary.BITS5 & info) !== 0) {
 | 
					      } else if ((binary.BITS5 & info) !== 0) {
 | 
				
			||||||
@ -62,25 +64,27 @@ function * lazyStructReaderGenerator (decoder) {
 | 
				
			|||||||
export class LazyStructReader {
 | 
					export class LazyStructReader {
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
 | 
					   * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
 | 
				
			||||||
 | 
					   * @param {boolean} filterSkips
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  constructor (decoder) {
 | 
					  constructor (decoder, filterSkips) {
 | 
				
			||||||
    this.gen = lazyStructReaderGenerator(decoder)
 | 
					    this.gen = lazyStructReaderGenerator(decoder)
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * @type {null | Item | GC}
 | 
					     * @type {null | Item | Skip | GC}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    this.curr = null
 | 
					    this.curr = null
 | 
				
			||||||
    this.done = false
 | 
					    this.done = false
 | 
				
			||||||
 | 
					    this.filterSkips = filterSkips
 | 
				
			||||||
    this.next()
 | 
					    this.next()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * @return {Item | GC | null}
 | 
					   * @return {Item | GC | Skip |null}
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  next () {
 | 
					  next () {
 | 
				
			||||||
    // ignore "Skip" structs
 | 
					    // ignore "Skip" structs
 | 
				
			||||||
    do {
 | 
					    do {
 | 
				
			||||||
      this.curr = this.gen.next().value || null
 | 
					      this.curr = this.gen.next().value || null
 | 
				
			||||||
    } while (this.curr !== null && this.curr.constructor === Skip)
 | 
					    } while (this.filterSkips && this.curr !== null && this.curr.constructor === Skip)
 | 
				
			||||||
    return this.curr
 | 
					    return this.curr
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -99,7 +103,7 @@ export const logUpdate = update => logUpdateV2(update, UpdateDecoderV1)
 | 
				
			|||||||
export const logUpdateV2 = (update, YDecoder = UpdateDecoderV2) => {
 | 
					export const logUpdateV2 = (update, YDecoder = UpdateDecoderV2) => {
 | 
				
			||||||
  const structs = []
 | 
					  const structs = []
 | 
				
			||||||
  const updateDecoder = new YDecoder(decoding.createDecoder(update))
 | 
					  const updateDecoder = new YDecoder(decoding.createDecoder(update))
 | 
				
			||||||
  const lazyDecoder = new LazyStructReader(updateDecoder)
 | 
					  const lazyDecoder = new LazyStructReader(updateDecoder, false)
 | 
				
			||||||
  for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) {
 | 
					  for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) {
 | 
				
			||||||
    structs.push(curr)
 | 
					    structs.push(curr)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -145,7 +149,7 @@ export const mergeUpdates = updates => mergeUpdatesV2(updates, UpdateDecoderV1,
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
export const encodeStateVectorFromUpdateV2 = (update, YEncoder = DSEncoderV2, YDecoder = UpdateDecoderV2) => {
 | 
					export const encodeStateVectorFromUpdateV2 = (update, YEncoder = DSEncoderV2, YDecoder = UpdateDecoderV2) => {
 | 
				
			||||||
  const encoder = new YEncoder()
 | 
					  const encoder = new YEncoder()
 | 
				
			||||||
  const updateDecoder = new LazyStructReader(new YDecoder(decoding.createDecoder(update)))
 | 
					  const updateDecoder = new LazyStructReader(new YDecoder(decoding.createDecoder(update)), true)
 | 
				
			||||||
  let curr = updateDecoder.curr
 | 
					  let curr = updateDecoder.curr
 | 
				
			||||||
  if (curr !== null) {
 | 
					  if (curr !== null) {
 | 
				
			||||||
    let size = 1
 | 
					    let size = 1
 | 
				
			||||||
@ -204,7 +208,7 @@ export const parseUpdateMetaV2 = (update, YDecoder = UpdateDecoderV2) => {
 | 
				
			|||||||
   * @type {Map<number, number>}
 | 
					   * @type {Map<number, number>}
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  const to = new Map()
 | 
					  const to = new Map()
 | 
				
			||||||
  const updateDecoder = new LazyStructReader(new YDecoder(decoding.createDecoder(update)))
 | 
					  const updateDecoder = new LazyStructReader(new YDecoder(decoding.createDecoder(update)), false)
 | 
				
			||||||
  let curr = updateDecoder.curr
 | 
					  let curr = updateDecoder.curr
 | 
				
			||||||
  if (curr !== null) {
 | 
					  if (curr !== null) {
 | 
				
			||||||
    let currClient = curr.id.client
 | 
					    let currClient = curr.id.client
 | 
				
			||||||
@ -277,7 +281,7 @@ const sliceStruct = (left, diff) => {
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => {
 | 
					export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => {
 | 
				
			||||||
  const updateDecoders = updates.map(update => new YDecoder(decoding.createDecoder(update)))
 | 
					  const updateDecoders = updates.map(update => new YDecoder(decoding.createDecoder(update)))
 | 
				
			||||||
  let lazyStructDecoders = updateDecoders.map(decoder => new LazyStructReader(decoder))
 | 
					  let lazyStructDecoders = updateDecoders.map(decoder => new LazyStructReader(decoder, true))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * @todo we don't need offset because we always slice before
 | 
					   * @todo we don't need offset because we always slice before
 | 
				
			||||||
@ -395,12 +399,51 @@ export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = U
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @param {Uint8Array} update
 | 
					 * @param {Uint8Array} update
 | 
				
			||||||
 * @param {Uint8Array} [sv]
 | 
					 * @param {Uint8Array} sv
 | 
				
			||||||
 | 
					 * @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} [YDecoder]
 | 
				
			||||||
 | 
					 * @param {typeof UpdateEncoderV1 | typeof UpdateEncoderV2} [YEncoder]
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export const diffUpdate = (update, sv = new Uint8Array([0])) => {
 | 
					export const diffUpdateV2 = (update, sv, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => {
 | 
				
			||||||
  // @todo!!!
 | 
					  const state = decodeStateVector(sv)
 | 
				
			||||||
  return update
 | 
					  const encoder = new YEncoder()
 | 
				
			||||||
 | 
					  const lazyStructWriter = new LazyStructWriter(encoder)
 | 
				
			||||||
 | 
					  const decoder = new YDecoder(decoding.createDecoder(update))
 | 
				
			||||||
 | 
					  const reader = new LazyStructReader(decoder, false)
 | 
				
			||||||
 | 
					  while (reader.curr) {
 | 
				
			||||||
 | 
					    const curr = reader.curr
 | 
				
			||||||
 | 
					    const currClient = curr.id.client
 | 
				
			||||||
 | 
					    const svClock = state.get(currClient) || 0
 | 
				
			||||||
 | 
					    if (reader.curr.constructor === Skip) {
 | 
				
			||||||
 | 
					      // the first written struct shouldn't be a skip
 | 
				
			||||||
 | 
					      reader.next()
 | 
				
			||||||
 | 
					      continue
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (curr.id.clock + curr.length > svClock) {
 | 
				
			||||||
 | 
					      writeStructToLazyStructWriter(lazyStructWriter, curr, math.max(svClock - curr.id.clock, 0))
 | 
				
			||||||
 | 
					      reader.next()
 | 
				
			||||||
 | 
					      while (reader.curr && reader.curr.id.client === currClient) {
 | 
				
			||||||
 | 
					        writeStructToLazyStructWriter(lazyStructWriter, reader.curr, 0)
 | 
				
			||||||
 | 
					        reader.next()
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // read until something new comes up
 | 
				
			||||||
 | 
					      while (reader.curr && reader.curr.id.client === currClient && reader.curr.id.clock + reader.curr.length <= svClock) {
 | 
				
			||||||
 | 
					        reader.next()
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  finishLazyStructWriting(lazyStructWriter)
 | 
				
			||||||
 | 
					  // write ds
 | 
				
			||||||
 | 
					  const ds = readDeleteSet(decoder)
 | 
				
			||||||
 | 
					  writeDeleteSet(encoder, ds)
 | 
				
			||||||
 | 
					  return encoder.toUint8Array()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @param {Uint8Array} update
 | 
				
			||||||
 | 
					 * @param {Uint8Array} sv
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const diffUpdate = (update, sv) => diffUpdateV2(update, sv, UpdateDecoderV1, UpdateEncoderV1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @param {LazyStructWriter} lazyWriter
 | 
					 * @param {LazyStructWriter} lazyWriter
 | 
				
			||||||
@ -428,7 +471,7 @@ const writeStructToLazyStructWriter = (lazyWriter, struct, offset) => {
 | 
				
			|||||||
    // write next client
 | 
					    // write next client
 | 
				
			||||||
    lazyWriter.encoder.writeClient(struct.id.client)
 | 
					    lazyWriter.encoder.writeClient(struct.id.client)
 | 
				
			||||||
    // write startClock
 | 
					    // write startClock
 | 
				
			||||||
    encoding.writeVarUint(lazyWriter.encoder.restEncoder, struct.id.clock)
 | 
					    encoding.writeVarUint(lazyWriter.encoder.restEncoder, struct.id.clock + offset)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  struct.write(lazyWriter.encoder, offset)
 | 
					  struct.write(lazyWriter.encoder, offset)
 | 
				
			||||||
  lazyWriter.written++
 | 
					  lazyWriter.written++
 | 
				
			||||||
 | 
				
			|||||||
@ -34,7 +34,8 @@ export const encV1 = {
 | 
				
			|||||||
  mergeUpdates: Y.mergeUpdates,
 | 
					  mergeUpdates: Y.mergeUpdates,
 | 
				
			||||||
  applyUpdate: Y.applyUpdate,
 | 
					  applyUpdate: Y.applyUpdate,
 | 
				
			||||||
  logUpdate: Y.logUpdate,
 | 
					  logUpdate: Y.logUpdate,
 | 
				
			||||||
  updateEventName: 'update'
 | 
					  updateEventName: 'update',
 | 
				
			||||||
 | 
					  diffUpdate: Y.diffUpdate
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const encV2 = {
 | 
					export const encV2 = {
 | 
				
			||||||
@ -42,7 +43,8 @@ export const encV2 = {
 | 
				
			|||||||
  mergeUpdates: Y.mergeUpdatesV2,
 | 
					  mergeUpdates: Y.mergeUpdatesV2,
 | 
				
			||||||
  applyUpdate: Y.applyUpdateV2,
 | 
					  applyUpdate: Y.applyUpdateV2,
 | 
				
			||||||
  logUpdate: Y.logUpdateV2,
 | 
					  logUpdate: Y.logUpdateV2,
 | 
				
			||||||
  updateEventName: 'updateV2'
 | 
					  updateEventName: 'updateV2',
 | 
				
			||||||
 | 
					  diffUpdate: Y.diffUpdateV2
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export let enc = encV1
 | 
					export let enc = encV1
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,9 @@
 | 
				
			|||||||
import * as t from 'lib0/testing.js'
 | 
					import * as t from 'lib0/testing.js'
 | 
				
			||||||
import { init, compare } from './testHelper.js' // eslint-disable-line
 | 
					import { init, compare } from './testHelper.js' // eslint-disable-line
 | 
				
			||||||
import * as Y from '../src/index.js'
 | 
					import * as Y from '../src/index.js'
 | 
				
			||||||
 | 
					import { readClientsStructRefs, readDeleteSet, UpdateDecoderV2, UpdateEncoderV2, writeDeleteSet } from '../src/internals.js'
 | 
				
			||||||
 | 
					import * as encoding from 'lib0/encoding.js'
 | 
				
			||||||
 | 
					import * as decoding from 'lib0/decoding.js'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @typedef {Object} Enc
 | 
					 * @typedef {Object} Enc
 | 
				
			||||||
@ -13,6 +16,7 @@ import * as Y from '../src/index.js'
 | 
				
			|||||||
 * @property {function(Uint8Array):Uint8Array} Enc.encodeStateVectorFromUpdate
 | 
					 * @property {function(Uint8Array):Uint8Array} Enc.encodeStateVectorFromUpdate
 | 
				
			||||||
 * @property {string} Enc.updateEventName
 | 
					 * @property {string} Enc.updateEventName
 | 
				
			||||||
 * @property {string} Enc.description
 | 
					 * @property {string} Enc.description
 | 
				
			||||||
 | 
					 * @property {function(Uint8Array, Uint8Array):Uint8Array} Enc.diffUpdate
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@ -27,7 +31,8 @@ const encV1 = {
 | 
				
			|||||||
  encodeStateVectorFromUpdate: Y.encodeStateVectorFromUpdate,
 | 
					  encodeStateVectorFromUpdate: Y.encodeStateVectorFromUpdate,
 | 
				
			||||||
  encodeStateVector: Y.encodeStateVector,
 | 
					  encodeStateVector: Y.encodeStateVector,
 | 
				
			||||||
  updateEventName: 'update',
 | 
					  updateEventName: 'update',
 | 
				
			||||||
  description: 'V1'
 | 
					  description: 'V1',
 | 
				
			||||||
 | 
					  diffUpdate: Y.diffUpdate
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@ -40,9 +45,10 @@ const encV2 = {
 | 
				
			|||||||
  logUpdate: Y.logUpdateV2,
 | 
					  logUpdate: Y.logUpdateV2,
 | 
				
			||||||
  parseUpdateMeta: Y.parseUpdateMetaV2,
 | 
					  parseUpdateMeta: Y.parseUpdateMetaV2,
 | 
				
			||||||
  encodeStateVectorFromUpdate: Y.encodeStateVectorFromUpdateV2,
 | 
					  encodeStateVectorFromUpdate: Y.encodeStateVectorFromUpdateV2,
 | 
				
			||||||
  encodeStateVector: Y.encodeStateVectorV2,
 | 
					  encodeStateVector: Y.encodeStateVector,
 | 
				
			||||||
  updateEventName: 'updateV2',
 | 
					  updateEventName: 'updateV2',
 | 
				
			||||||
  description: 'V2'
 | 
					  description: 'V2',
 | 
				
			||||||
 | 
					  diffUpdate: Y.diffUpdateV2
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@ -50,7 +56,7 @@ const encV2 = {
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
const encDoc = {
 | 
					const encDoc = {
 | 
				
			||||||
  mergeUpdates: (updates) => {
 | 
					  mergeUpdates: (updates) => {
 | 
				
			||||||
    const ydoc = new Y.Doc()
 | 
					    const ydoc = new Y.Doc({ gc: false })
 | 
				
			||||||
    updates.forEach(update => {
 | 
					    updates.forEach(update => {
 | 
				
			||||||
      Y.applyUpdateV2(ydoc, update)
 | 
					      Y.applyUpdateV2(ydoc, update)
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
@ -61,9 +67,18 @@ const encDoc = {
 | 
				
			|||||||
  logUpdate: Y.logUpdateV2,
 | 
					  logUpdate: Y.logUpdateV2,
 | 
				
			||||||
  parseUpdateMeta: Y.parseUpdateMetaV2,
 | 
					  parseUpdateMeta: Y.parseUpdateMetaV2,
 | 
				
			||||||
  encodeStateVectorFromUpdate: Y.encodeStateVectorFromUpdateV2,
 | 
					  encodeStateVectorFromUpdate: Y.encodeStateVectorFromUpdateV2,
 | 
				
			||||||
  encodeStateVector: Y.encodeStateVectorV2,
 | 
					  encodeStateVector: Y.encodeStateVector,
 | 
				
			||||||
  updateEventName: 'updateV2',
 | 
					  updateEventName: 'updateV2',
 | 
				
			||||||
  description: 'Merge via Y.Doc'
 | 
					  description: 'Merge via Y.Doc',
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * @param {Uint8Array} update
 | 
				
			||||||
 | 
					   * @param {Uint8Array} sv
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  diffUpdate: (update, sv) => {
 | 
				
			||||||
 | 
					    const ydoc = new Y.Doc({ gc: false })
 | 
				
			||||||
 | 
					    Y.applyUpdateV2(ydoc, update)
 | 
				
			||||||
 | 
					    return Y.encodeStateAsUpdateV2(ydoc, sv)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const encoders = [encV1, encV2, encDoc]
 | 
					const encoders = [encV1, encV2, encDoc]
 | 
				
			||||||
@ -101,8 +116,9 @@ export const testMergeUpdates = tc => {
 | 
				
			|||||||
 * @param {Y.Doc} ydoc
 | 
					 * @param {Y.Doc} ydoc
 | 
				
			||||||
 * @param {Array<Uint8Array>} updates - expecting at least 4 updates
 | 
					 * @param {Array<Uint8Array>} updates - expecting at least 4 updates
 | 
				
			||||||
 * @param {Enc} enc
 | 
					 * @param {Enc} enc
 | 
				
			||||||
 | 
					 * @param {boolean} hasDeletes
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
const checkUpdateCases = (ydoc, updates, enc) => {
 | 
					const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
 | 
				
			||||||
  const cases = []
 | 
					  const cases = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Case 1: Simple case, simply merge everything
 | 
					  // Case 1: Simple case, simply merge everything
 | 
				
			||||||
@ -135,14 +151,47 @@ const checkUpdateCases = (ydoc, updates, enc) => {
 | 
				
			|||||||
  // t.info('Target State: ')
 | 
					  // t.info('Target State: ')
 | 
				
			||||||
  // enc.logUpdate(targetState)
 | 
					  // enc.logUpdate(targetState)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  cases.forEach((updates, i) => {
 | 
					  cases.forEach((mergedUpdates, i) => {
 | 
				
			||||||
    // t.info('State Case $' + i + ':')
 | 
					    // t.info('State Case $' + i + ':')
 | 
				
			||||||
    // enc.logUpdate(updates)
 | 
					    // enc.logUpdate(updates)
 | 
				
			||||||
    const merged = new Y.Doc()
 | 
					    const merged = new Y.Doc({ gc: false })
 | 
				
			||||||
    enc.applyUpdate(merged, updates)
 | 
					    enc.applyUpdate(merged, mergedUpdates)
 | 
				
			||||||
    t.compareArrays(merged.getArray().toArray(), ydoc.getArray().toArray())
 | 
					    t.compareArrays(merged.getArray().toArray(), ydoc.getArray().toArray())
 | 
				
			||||||
    t.compare(enc.encodeStateVector(merged), enc.encodeStateVectorFromUpdate(updates))
 | 
					    t.compare(enc.encodeStateVector(merged), enc.encodeStateVectorFromUpdate(mergedUpdates))
 | 
				
			||||||
    const meta = enc.parseUpdateMeta(updates)
 | 
					
 | 
				
			||||||
 | 
					    if (enc.updateEventName !== 'update') { // @todo should this also work on legacy updates?
 | 
				
			||||||
 | 
					      for (let j = 1; j < updates.length; j++) {
 | 
				
			||||||
 | 
					        const partMerged = enc.mergeUpdates(updates.slice(j))
 | 
				
			||||||
 | 
					        const partMeta = enc.parseUpdateMeta(partMerged)
 | 
				
			||||||
 | 
					        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)
 | 
				
			||||||
 | 
					          // because diffed contains the set of all deletes.
 | 
				
			||||||
 | 
					          // So we add all deletes from `diffed` to `partDeletes` and compare then
 | 
				
			||||||
 | 
					          const decoder = decoding.createDecoder(diffed)
 | 
				
			||||||
 | 
					          const updateDecoder = new UpdateDecoderV2(decoder)
 | 
				
			||||||
 | 
					          readClientsStructRefs(updateDecoder, new Y.Doc())
 | 
				
			||||||
 | 
					          const ds = readDeleteSet(updateDecoder)
 | 
				
			||||||
 | 
					          const updateEncoder = new UpdateEncoderV2()
 | 
				
			||||||
 | 
					          encoding.writeVarUint(updateEncoder.restEncoder, 0) // 0 structs
 | 
				
			||||||
 | 
					          writeDeleteSet(updateEncoder, ds)
 | 
				
			||||||
 | 
					          const deletesUpdate = updateEncoder.toUint8Array()
 | 
				
			||||||
 | 
					          const mergedDeletes = Y.mergeUpdatesV2([deletesUpdate, partMerged])
 | 
				
			||||||
 | 
					          if (!hasDeletes || enc !== encDoc) {
 | 
				
			||||||
 | 
					            // deletes will almost definitely lead to different encoders because of the mergeStruct feature that is present in encDoc
 | 
				
			||||||
 | 
					            t.compare(diffed, mergedDeletes)
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const meta = enc.parseUpdateMeta(mergedUpdates)
 | 
				
			||||||
    meta.from.forEach((clock, client) => t.assert(clock === 0))
 | 
					    meta.from.forEach((clock, client) => t.assert(clock === 0))
 | 
				
			||||||
    meta.to.forEach((clock, client) => {
 | 
					    meta.to.forEach((clock, client) => {
 | 
				
			||||||
      const structs = /** @type {Array<Y.Item>} */ (merged.store.clients.get(client))
 | 
					      const structs = /** @type {Array<Y.Item>} */ (merged.store.clients.get(client))
 | 
				
			||||||
@ -168,7 +217,7 @@ export const testMergeUpdates1 = tc => {
 | 
				
			|||||||
    array.insert(0, [3])
 | 
					    array.insert(0, [3])
 | 
				
			||||||
    array.insert(0, [4])
 | 
					    array.insert(0, [4])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    checkUpdateCases(ydoc, updates, enc)
 | 
					    checkUpdateCases(ydoc, updates, enc, false)
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -188,7 +237,7 @@ export const testMergeUpdates2 = tc => {
 | 
				
			|||||||
    array.insert(0, [3, 4])
 | 
					    array.insert(0, [3, 4])
 | 
				
			||||||
    array.delete(1, 2)
 | 
					    array.delete(1, 2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    checkUpdateCases(ydoc, updates, enc)
 | 
					    checkUpdateCases(ydoc, updates, enc, true)
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user