Create Structs based on offset, if necessary

implement offset parameter in Ref.toStruct
This commit is contained in:
Kevin Jahns 2019-04-05 12:38:02 +02:00
parent e56899a02c
commit 8a7416ad50
15 changed files with 234 additions and 74 deletions

View File

@ -139,9 +139,6 @@ export class AbstractItem extends AbstractStruct {
const parent = this.parent const parent = this.parent
const parentSub = this.parentSub const parentSub = this.parentSub
const length = this.length const length = this.length
const left = this.left
const right = this.right
// integrate
/* /*
# $this has to find a unique position between origin and the next known character # $this has to find a unique position between origin and the next known character
# case 1: $origin equals $o.origin: the $creator parameter decides if left or right # case 1: $origin equals $o.origin: the $creator parameter decides if left or right
@ -163,8 +160,8 @@ export class AbstractItem extends AbstractStruct {
*/ */
let o let o
// set o to the first conflicting item // set o to the first conflicting item
if (left !== null) { if (this.left !== null) {
o = left.right o = this.left.right
} else if (parentSub !== null) { } else if (parentSub !== null) {
o = parent._map.get(parentSub) || null o = parent._map.get(parentSub) || null
} else { } else {
@ -175,7 +172,7 @@ export class AbstractItem extends AbstractStruct {
// Let c in conflictingItems, b in itemsBeforeOrigin // Let c in conflictingItems, b in itemsBeforeOrigin
// ***{origin}bbbb{this}{c,b}{c,b}{o}*** // ***{origin}bbbb{this}{c,b}{c,b}{o}***
// Note that conflictingItems is a subset of itemsBeforeOrigin // Note that conflictingItems is a subset of itemsBeforeOrigin
while (o !== null && o !== right) { while (o !== null && o !== this.right) {
itemsBeforeOrigin.add(o) itemsBeforeOrigin.add(o)
conflictingItems.add(o) conflictingItems.add(o)
if (this.origin === o.origin) { if (this.origin === o.origin) {
@ -199,10 +196,10 @@ export class AbstractItem extends AbstractStruct {
o = o.right o = o.right
} }
// reconnect left/right + update parent map/start if necessary // reconnect left/right + update parent map/start if necessary
if (left !== null) { if (this.left !== null) {
const right = left.right const right = this.left.right
this.right = right this.right = right
left.right = this this.left.right = this
if (right !== null) { if (right !== null) {
right.left = this right.left = this
} }
@ -230,12 +227,12 @@ export class AbstractItem extends AbstractStruct {
maplib.setIfUndefined(transaction.changed, parent, set.create).add(parentSub) maplib.setIfUndefined(transaction.changed, parent, set.create).add(parentSub)
} }
// @ts-ignore // @ts-ignore
if ((parent._item !== null && parent._item.deleted) || (left !== null && parentSub !== null)) { if ((parent._item !== null && parent._item.deleted) || (this.left !== null && parentSub !== null)) {
// delete if parent is deleted or if this is not the current attribute value of parent // delete if parent is deleted or if this is not the current attribute value of parent
this.delete(transaction) this.delete(transaction)
} else if (parentSub !== null && left === null && right !== null) { } else if (parentSub !== null && this.left === null && this.right !== null) {
// this is the current attribute value of parent. delete right // this is the current attribute value of parent. delete right
right.delete(transaction) this.right.delete(transaction)
} }
} }
@ -529,3 +526,12 @@ export class AbstractItemRef extends AbstractRef {
} }
} }
} }
/**
* @param {AbstractItemRef} item
* @param {number} offset
*/
export const changeItemRefOffset = (item, offset) => {
item.id = createID(item.id.client, item.id.clock + offset)
item.left = createID(item.id.client, item.id.clock - 1)
}

View File

@ -1,6 +1,6 @@
import { import {
Y, ID, Transaction // eslint-disable-line ID, Transaction // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as encoding from 'lib0/encoding.js' // eslint-disable-line import * as encoding from 'lib0/encoding.js' // eslint-disable-line
@ -77,9 +77,10 @@ export class AbstractRef {
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {number} offset
* @return {AbstractStruct} * @return {AbstractStruct}
*/ */
toStruct (transaction) { toStruct (transaction, offset) {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/** /**

View File

@ -6,7 +6,8 @@ import {
AbstractStruct, AbstractStruct,
createID, createID,
writeID, writeID,
ID // eslint-disable-line addStruct,
Transaction, ID // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as decoding from 'lib0/decoding.js' import * as decoding from 'lib0/decoding.js'
@ -42,6 +43,13 @@ export class GC extends AbstractStruct {
return true return true
} }
/**
* @param {Transaction} transaction
*/
integrate (transaction) {
addStruct(transaction.y.store, this)
}
/** /**
* @param {encoding.Encoder} encoder * @param {encoding.Encoder} encoder
* @param {number} offset * @param {number} offset
@ -65,10 +73,6 @@ export class GCRef extends AbstractRef {
*/ */
constructor (decoder, id, info) { constructor (decoder, id, info) {
super(id) super(id)
/**
* @type {ID}
*/
this.id = id
/** /**
* @type {number} * @type {number}
*/ */
@ -83,9 +87,16 @@ export class GCRef extends AbstractRef {
] ]
} }
/** /**
* @param {Transaction} transaction
* @param {number} offset
* @return {GC} * @return {GC}
*/ */
toStruct () { toStruct (transaction, offset) {
if (offset > 0) {
// @ts-ignore
this.id = createID(this.id.client, this.id.clock + offset)
this._len = this._len - offset
}
return new GC( return new GC(
this.id, this.id,
this._len this._len

View File

@ -10,6 +10,8 @@ import {
getItemCleanEnd, getItemCleanEnd,
getItemCleanStart, getItemCleanStart,
getItemType, getItemType,
GC,
ItemDeleted,
Transaction, ID, AbstractType // eslint-disable-line Transaction, ID, AbstractType // eslint-disable-line
} from '../internals.js' } from '../internals.js'
@ -69,17 +71,32 @@ export class ItemBinaryRef extends AbstractItemRef {
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @return {ItemBinary} * @param {number} offset
* @return {ItemBinary|GC}
*/ */
toStruct (transaction) { toStruct (transaction, offset) {
const y = transaction.y const y = transaction.y
const store = y.store const store = y.store
let parent
if (this.parent !== null) {
const parentItem = getItemType(store, this.parent)
switch (parentItem.constructor) {
case ItemDeleted:
case GC:
return new GC(this.id, 1)
}
parent = parentItem.type
} else {
// @ts-ignore
parent = y.get(this.parentYKey)
}
return new ItemBinary( return new ItemBinary(
this.id, this.id,
this.left === null ? null : getItemCleanEnd(store, transaction, this.left), this.left === null ? null : getItemCleanEnd(store, transaction, this.left),
this.right === null ? null : getItemCleanStart(store, transaction, this.right), this.right === null ? null : getItemCleanStart(store, transaction, this.right),
// @ts-ignore parent,
this.parent === null ? y.get(this.parentYKey) : getItemType(store, this.parent).type,
this.parentSub, this.parentSub,
this.content this.content
) )

View File

@ -10,6 +10,8 @@ import {
getItemCleanEnd, getItemCleanEnd,
getItemCleanStart, getItemCleanStart,
getItemType, getItemType,
changeItemRefOffset,
GC,
Transaction, ID, AbstractType // eslint-disable-line Transaction, ID, AbstractType // eslint-disable-line
} from '../internals.js' } from '../internals.js'
@ -84,17 +86,36 @@ export class ItemDeletedRef extends AbstractItemRef {
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @return {ItemDeleted} * @param {number} offset
* @return {ItemDeleted|GC}
*/ */
toStruct (transaction) { toStruct (transaction, offset) {
const y = transaction.y const y = transaction.y
const store = y.store const store = y.store
if (offset > 0) {
changeItemRefOffset(this, offset)
this.len = this.len - offset
}
let parent
if (this.parent !== null) {
const parentItem = getItemType(store, this.parent)
switch (parentItem.constructor) {
case ItemDeleted:
case GC:
return new GC(this.id, 1)
}
parent = parentItem.type
} else {
// @ts-ignore
parent = y.get(this.parentYKey)
}
return new ItemDeleted( return new ItemDeleted(
this.id, this.id,
this.left === null ? null : getItemCleanEnd(store, transaction, this.left), this.left === null ? null : getItemCleanEnd(store, transaction, this.left),
this.right === null ? null : getItemCleanStart(store, transaction, this.right), this.right === null ? null : getItemCleanStart(store, transaction, this.right),
// @ts-ignore parent,
this.parent === null ? y.get(this.parentYKey) : getItemType(store, this.parent).type,
this.parentSub, this.parentSub,
this.len this.len
) )

View File

@ -8,6 +8,8 @@ import {
getItemCleanEnd, getItemCleanEnd,
getItemCleanStart, getItemCleanStart,
getItemType, getItemType,
ItemDeleted,
GC,
Transaction, ID, AbstractType // eslint-disable-line Transaction, ID, AbstractType // eslint-disable-line
} from '../internals.js' } from '../internals.js'
@ -64,17 +66,32 @@ export class ItemEmbedRef extends AbstractItemRef {
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @return {ItemEmbed} * @param {number} offset
* @return {ItemEmbed|GC}
*/ */
toStruct (transaction) { toStruct (transaction, offset) {
const y = transaction.y const y = transaction.y
const store = y.store const store = y.store
let parent
if (this.parent !== null) {
const parentItem = getItemType(store, this.parent)
switch (parentItem.constructor) {
case ItemDeleted:
case GC:
return new GC(this.id, 1)
}
parent = parentItem.type
} else {
// @ts-ignore
parent = y.get(this.parentYKey)
}
return new ItemEmbed( return new ItemEmbed(
this.id, this.id,
this.left === null ? null : getItemCleanEnd(store, transaction, this.left), this.left === null ? null : getItemCleanEnd(store, transaction, this.left),
this.right === null ? null : getItemCleanStart(store, transaction, this.right), this.right === null ? null : getItemCleanStart(store, transaction, this.right),
// @ts-ignore parent,
this.parent === null ? y.get(this.parentYKey) : getItemType(store, this.parent).type,
this.parentSub, this.parentSub,
this.embed this.embed
) )

View File

@ -8,6 +8,8 @@ import {
getItemCleanEnd, getItemCleanEnd,
getItemCleanStart, getItemCleanStart,
getItemType, getItemType,
ItemDeleted,
GC,
Transaction, ID, AbstractType // eslint-disable-line Transaction, ID, AbstractType // eslint-disable-line
} from '../internals.js' } from '../internals.js'
@ -71,17 +73,32 @@ export class ItemFormatRef extends AbstractItemRef {
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @return {ItemFormat} * @param {number} offset
* @return {ItemFormat|GC}
*/ */
toStruct (transaction) { toStruct (transaction, offset) {
const y = transaction.y const y = transaction.y
const store = y.store const store = y.store
let parent
if (this.parent !== null) {
const parentItem = getItemType(store, this.parent)
switch (parentItem.constructor) {
case ItemDeleted:
case GC:
return new GC(this.id, 1)
}
parent = parentItem.type
} else {
// @ts-ignore
parent = y.get(this.parentYKey)
}
return new ItemFormat( return new ItemFormat(
this.id, this.id,
this.left === null ? null : getItemCleanEnd(store, transaction, this.left), this.left === null ? null : getItemCleanEnd(store, transaction, this.left),
this.right === null ? null : getItemCleanStart(store, transaction, this.right), this.right === null ? null : getItemCleanStart(store, transaction, this.right),
// @ts-ignore parent,
this.parent === null ? y.get(this.parentYKey) : getItemType(store, this.parent).type,
this.parentSub, this.parentSub,
this.key, this.key,
this.value this.value

View File

@ -9,6 +9,9 @@ import {
getItemCleanStart, getItemCleanStart,
getItemType, getItemType,
splitItem, splitItem,
changeItemRefOffset,
GC,
ItemDeleted,
Transaction, ID, AbstractType // eslint-disable-line Transaction, ID, AbstractType // eslint-disable-line
} from '../internals.js' } from '../internals.js'
@ -106,6 +109,9 @@ export class ItemJSONRef extends AbstractItemRef {
cs.push(JSON.parse(c)) cs.push(JSON.parse(c))
} }
} }
/**
* @type {Array<any>}
*/
this.content = cs this.content = cs
} }
get length () { get length () {
@ -113,17 +119,34 @@ export class ItemJSONRef extends AbstractItemRef {
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @return {ItemJSON} * @param {number} offset
* @return {ItemJSON|GC}
*/ */
toStruct (transaction) { toStruct (transaction, offset) {
const y = transaction.y const y = transaction.y
const store = y.store const store = y.store
if (offset > 0) {
changeItemRefOffset(this, offset)
this.content = this.content.slice(offset)
}
let parent
if (this.parent !== null) {
const parentItem = getItemType(store, this.parent)
switch (parentItem.constructor) {
case ItemDeleted:
case GC:
return new GC(this.id, this.content.length)
}
parent = parentItem.type
} else {
// @ts-ignore
parent = y.get(this.parentYKey)
}
return new ItemJSON( return new ItemJSON(
this.id, this.id,
this.left === null ? null : getItemCleanEnd(store, transaction, this.left), this.left === null ? null : getItemCleanEnd(store, transaction, this.left),
this.right === null ? null : getItemCleanStart(store, transaction, this.right), this.right === null ? null : getItemCleanStart(store, transaction, this.right),
// @ts-ignore parent,
this.parent === null ? y.get(this.parentYKey) : getItemType(store, this.parent).type,
this.parentSub, this.parentSub,
this.content this.content
) )

View File

@ -8,6 +8,9 @@ import {
getItemCleanStart, getItemCleanStart,
getItemType, getItemType,
splitItem, splitItem,
changeItemRefOffset,
ItemDeleted,
GC,
Transaction, ID, AbstractType // eslint-disable-line Transaction, ID, AbstractType // eslint-disable-line
} from '../internals.js' } from '../internals.js'
@ -102,17 +105,36 @@ export class ItemStringRef extends AbstractItemRef {
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @return {ItemString} * @param {number} offset
* @return {ItemString|GC}
*/ */
toStruct (transaction) { toStruct (transaction, offset) {
const y = transaction.y const y = transaction.y
const store = y.store const store = y.store
if (offset > 0) {
changeItemRefOffset(this, offset)
this.string = this.string.slice(offset)
}
let parent
if (this.parent !== null) {
const parentItem = getItemType(store, this.parent)
switch (parentItem.constructor) {
case ItemDeleted:
case GC:
return new GC(this.id, this.string.length)
}
parent = parentItem.type
} else {
// @ts-ignore
parent = y.get(this.parentYKey)
}
return new ItemString( return new ItemString(
this.id, this.id,
this.left === null ? null : getItemCleanEnd(store, transaction, this.left), this.left === null ? null : getItemCleanEnd(store, transaction, this.left),
this.right === null ? null : getItemCleanStart(store, transaction, this.right), this.right === null ? null : getItemCleanStart(store, transaction, this.right),
// @ts-ignore parent,
this.parent === null ? y.get(this.parentYKey) : getItemType(store, this.parent).type,
this.parentSub, this.parentSub,
this.string this.string
) )

View File

@ -168,17 +168,32 @@ export class ItemTypeRef extends AbstractItemRef {
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @return {ItemType} * @param {number} offset
* @return {ItemType|GC}
*/ */
toStruct (transaction) { toStruct (transaction, offset) {
const y = transaction.y const y = transaction.y
const store = y.store const store = y.store
let parent
if (this.parent !== null) {
const parentItem = getItemType(store, this.parent)
switch (parentItem.constructor) {
case ItemDeleted:
case GC:
return new GC(this.id, 1)
}
parent = parentItem.type
} else {
// @ts-ignore
parent = y.get(this.parentYKey)
}
return new ItemType( return new ItemType(
this.id, this.id,
this.left === null ? null : getItemCleanEnd(store, transaction, this.left), this.left === null ? null : getItemCleanEnd(store, transaction, this.left),
this.right === null ? null : getItemCleanStart(store, transaction, this.right), this.right === null ? null : getItemCleanStart(store, transaction, this.right),
// @ts-ignore parent,
this.parent === null ? y.get(this.parentYKey) : getItemType(store, this.parent).type,
this.parentSub, this.parentSub,
this.type this.type
) )

View File

@ -19,20 +19,20 @@ export class StructStore {
} }
/** /**
* Return the states as an array of {client,clock} pairs. * Return the states as a Map<client,clock>.
* Note that clock refers to the next expected clock id. * Note that clock refers to the next expected clock id.
* *
* @param {StructStore} store * @param {StructStore} store
* @return {Array<{client:number,clock:number}>} * @return {Map<number,number>}
*/ */
export const getStates = store => export const getStates = store => {
map.map(store.clients, (structs, client) => { const sm = new Map()
store.clients.forEach((structs, client) => {
const struct = structs[structs.length - 1] const struct = structs[structs.length - 1]
return { sm.set(client, struct.id.clock + struct.length)
client,
clock: struct.id.clock + struct.length
}
}) })
return sm
}
/** /**
* @param {StructStore} store * @param {StructStore} store

View File

@ -56,8 +56,11 @@ export class Transaction {
* Holds the state before the transaction started. * Holds the state before the transaction started.
* @type {Map<Number,Number>} * @type {Map<Number,Number>}
*/ */
this.beforeState = new Map() this.beforeState = getStates(y.store)
getStates(y.store).forEach(({client, clock}) => { this.beforeState.set(client, clock) }) /**
* Holds the state after the transaction.
* @type {Map<Number,Number>}
*/
this.afterState = new Map() this.afterState = new Map()
/** /**
* All types that were directly modified (property added or child * All types that were directly modified (property added or child

View File

@ -96,9 +96,7 @@ export class Y extends Observable {
// only call afterTransaction listeners if anything changed // only call afterTransaction listeners if anything changed
const transactionChangedContent = transaction.changedParentTypes.size !== 0 const transactionChangedContent = transaction.changedParentTypes.size !== 0
if (transactionChangedContent) { if (transactionChangedContent) {
getStates(transaction.y.store).forEach(({client, clock}) => { transaction.afterState = getStates(transaction.y.store)
transaction.afterState.set(client, clock)
})
// when all changes & events are processed, emit afterTransaction event // when all changes & events are processed, emit afterTransaction event
this.emit('afterTransaction', [this, transaction]) this.emit('afterTransaction', [this, transaction])
// transaction cleanup // transaction cleanup
@ -152,7 +150,8 @@ export class Y extends Observable {
// @ts-ignore // @ts-ignore
const structs = store.clients.get(client) const structs = store.clients.get(client)
// we iterate from right to left so we can safely remove entries // we iterate from right to left so we can safely remove entries
for (let i = structs.length - 1; i >= math.max(findIndexSS(structs, clock), 1); i--) { const firstChangePos = math.max(findIndexSS(structs, clock), 1)
for (let i = structs.length - 1; i >= firstChangePos; i--) {
tryToMergeWithLeft(structs, i) tryToMergeWithLeft(structs, i)
} }
} }

View File

@ -43,19 +43,23 @@ const structRefs = [
* @param {decoding.Decoder} decoder * @param {decoding.Decoder} decoder
* @param {number} structsLen * @param {number} structsLen
* @param {ID} nextID * @param {ID} nextID
* @param {number} localState next expected clock by nextID.client
* @return {Iterator<AbstractRef>} * @return {Iterator<AbstractRef>}
*/ */
const createStructReaderIterator = (decoder, structsLen, nextID) => iterator.createIterator(() => { const createStructReaderIterator = (decoder, structsLen, nextID, localState) => iterator.createIterator(() => {
let done = false let done = false
let value let value
if (structsLen === 0) { do {
done = true if (structsLen === 0) {
} else { done = true
value = undefined
break
}
const info = decoding.readUint8(decoder) const info = decoding.readUint8(decoder)
value = new structRefs[binary.BITS5 & info](decoder, nextID, info) value = new structRefs[binary.BITS5 & info](decoder, nextID, info)
nextID = createID(nextID.client, nextID.clock + value.length) nextID = createID(nextID.client, nextID.clock + value.length)
structsLen-- structsLen--
} } while (nextID.clock <= localState) // read until we find something new (check nextID.clock instead because it equals `clock+len`)
return { done, value } return { done, value }
}) })
@ -78,7 +82,7 @@ export const writeStructs = (encoder, store, _sm) => {
sm.set(client, clock) sm.set(client, clock)
} }
}) })
getStates(store).forEach(({client}) => { getStates(store).forEach((clock, client) => {
if (!_sm.has(client)) { if (!_sm.has(client)) {
sm.set(client, 0) sm.set(client, 0)
} }
@ -131,17 +135,18 @@ export const readStructs = (decoder, transaction, store) => {
*/ */
const structReaders = new Map() const structReaders = new Map()
const clientbeforeState = decoding.readVarUint(decoder) const clientbeforeState = decoding.readVarUint(decoder)
/**
* @type {Array<AbstractRef>}
*/
const stack = []
const localState = getStates(store)
for (let i = 0; i < clientbeforeState; i++) { for (let i = 0; i < clientbeforeState; i++) {
const nextID = readID(decoder) const nextID = readID(decoder)
const decoderPos = decoder.pos + decoding.readUint32(decoder) const decoderPos = decoder.pos + decoding.readUint32(decoder)
const structReaderDecoder = decoding.clone(decoder, decoderPos) const structReaderDecoder = decoding.clone(decoder, decoderPos)
const numberOfStructs = decoding.readVarUint(structReaderDecoder) const numberOfStructs = decoding.readVarUint(structReaderDecoder)
structReaders.set(nextID.client, createStructReaderIterator(structReaderDecoder, numberOfStructs, nextID)) structReaders.set(nextID.client, createStructReaderIterator(structReaderDecoder, numberOfStructs, nextID, localState.get(nextID.client) || 0))
} }
/**
* @type {Array<AbstractRef>}
*/
const stack = []
for (const it of structReaders.values()) { for (const it of structReaders.values()) {
// todo try for in of it // todo try for in of it
for (let res = it.next(); !res.done; res = it.next()) { for (let res = it.next(); !res.done; res = it.next()) {
@ -159,7 +164,9 @@ export const readStructs = (decoder, transaction, store) => {
ref._missing.pop() ref._missing.pop()
} }
if (m.length === 0) { if (m.length === 0) {
ref.toStruct(transaction).integrate(transaction) const localClock = (localState.get(ref.id.client) || 0)
const offset = ref.id.clock < localClock ? localClock - ref.id.clock : 0
ref.toStruct(transaction, offset).integrate(transaction)
stack.pop() stack.pop()
} }
} }

View File

@ -243,6 +243,7 @@ export const init = (tc, { users = 5 } = {}) => {
result.testConnector = testConnector result.testConnector = testConnector
for (let i = 0; i < users; i++) { for (let i = 0; i < users; i++) {
const y = testConnector.createY(i) const y = testConnector.createY(i)
y.clientID = i
result.users.push(y) result.users.push(y)
result['array' + i] = y.get('array', Y.Array) result['array' + i] = y.get('array', Y.Array)
result['map' + i] = y.get('map', Y.Map) result['map' + i] = y.get('map', Y.Map)