Improve memory usage by omitting the ItemRef step and directly applying the Item

This commit is contained in:
Kevin Jahns 2020-06-09 00:53:05 +02:00
parent 0a0098fdfb
commit 5293ab4df1
14 changed files with 302 additions and 348 deletions

View File

@ -39,39 +39,9 @@ export class AbstractStruct {
/**
* @param {Transaction} transaction
*/
integrate (transaction) {
throw error.methodUnimplemented()
}
}
export class AbstractStructRef {
/**
* @param {ID} id
*/
constructor (id) {
this.id = id
/**
* @type {Array<ID>}
*/
this._missing = []
}
/**
* @param {Transaction} transaction
* @return {Array<ID|null>}
*/
getMissing (transaction) {
return this._missing
}
/**
* @param {Transaction} transaction
* @param {StructStore} store
* @param {number} offset
* @return {AbstractStruct}
*/
toStruct (transaction, store, offset) {
integrate (transaction, offset) {
throw error.methodUnimplemented()
}
}

View File

@ -1,12 +1,10 @@
import {
AbstractStructRef,
AbstractStruct,
addStruct,
StructStore, Transaction, ID // eslint-disable-line
} from '../internals.js'
import * as decoding from 'lib0/decoding.js'
import * as encoding from 'lib0/encoding.js'
export const structGCRefNumber = 0
@ -37,8 +35,13 @@ export class GC extends AbstractStruct {
/**
* @param {Transaction} transaction
* @param {number} offset
*/
integrate (transaction) {
integrate (transaction, offset) {
if (offset > 0) {
this.id.clock += offset
this.length -= offset
}
addStruct(transaction.doc.store, this)
}
@ -50,39 +53,13 @@ export class GC extends AbstractStruct {
encoding.writeUint8(encoder, structGCRefNumber)
encoding.writeVarUint(encoder, this.length - offset)
}
}
/**
* @private
*/
export class GCRef extends AbstractStructRef {
/**
* @param {decoding.Decoder} decoder
* @param {ID} id
* @param {number} info
*/
constructor (decoder, id, info) {
super(id)
/**
* @type {number}
*/
this.length = decoding.readVarUint(decoder)
}
/**
* @param {Transaction} transaction
* @param {StructStore} store
* @param {number} offset
* @return {GC}
* @return {null | ID}
*/
toStruct (transaction, store, offset) {
if (offset > 0) {
this.id.clock += offset
this.length -= offset
}
return new GC(
this.id,
this.length
)
getMissing (transaction, store) {
return null
}
}

View File

@ -4,7 +4,6 @@ import {
writeID,
GC,
getState,
AbstractStructRef,
AbstractStruct,
replaceStruct,
addStruct,
@ -24,7 +23,7 @@ import {
readContentFormat,
readContentType,
addChangedTypeToTransaction,
ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line
Doc, ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line
} from '../internals.js'
import * as error from 'lib0/error.js'
@ -73,7 +72,7 @@ export const followRedone = (store, id) => {
export const keepItem = (item, keep) => {
while (item !== null && item.keep !== keep) {
item.keep = keep
item = item.parent._item
item = /** @type {AbstractType<any>} */ (item.parent)._item
}
}
@ -119,7 +118,7 @@ export const splitItem = (transaction, leftItem, diff) => {
transaction._mergeStructs.push(rightItem)
// update parent._map
if (rightItem.parentSub !== null && rightItem.right === null) {
rightItem.parent._map.set(rightItem.parentSub, rightItem)
/** @type {AbstractType<any>} */ (rightItem.parent)._map.set(rightItem.parentSub, rightItem)
}
leftItem.length = diff
return rightItem
@ -144,7 +143,7 @@ export const redoItem = (transaction, item, redoitems) => {
if (redone !== null) {
return getItemCleanStart(transaction, redone)
}
let parentItem = item.parent._item
let parentItem = /** @type {AbstractType<any>} */ (item.parent)._item
/**
* @type {Item|null}
*/
@ -169,7 +168,7 @@ export const redoItem = (transaction, item, redoitems) => {
}
}
if (left.right !== null) {
left = /** @type {Item} */ (item.parent._map.get(item.parentSub))
left = /** @type {Item} */ (/** @type {AbstractType<any>} */ (item.parent)._map.get(item.parentSub))
}
right = null
}
@ -191,10 +190,10 @@ export const redoItem = (transaction, item, redoitems) => {
*/
let leftTrace = left
// trace redone until parent matches
while (leftTrace !== null && leftTrace.parent._item !== parentItem) {
while (leftTrace !== null && /** @type {AbstractType<any>} */ (leftTrace.parent)._item !== parentItem) {
leftTrace = leftTrace.redone === null ? null : getItemCleanStart(transaction, leftTrace.redone)
}
if (leftTrace !== null && leftTrace.parent._item === parentItem) {
if (leftTrace !== null && /** @type {AbstractType<any>} */ (leftTrace.parent)._item === parentItem) {
left = leftTrace
break
}
@ -206,10 +205,10 @@ export const redoItem = (transaction, item, redoitems) => {
*/
let rightTrace = right
// trace redone until parent matches
while (rightTrace !== null && rightTrace.parent._item !== parentItem) {
while (rightTrace !== null && /** @type {AbstractType<any>} */ (rightTrace.parent)._item !== parentItem) {
rightTrace = rightTrace.redone === null ? null : getItemCleanStart(transaction, rightTrace.redone)
}
if (rightTrace !== null && rightTrace.parent._item === parentItem) {
if (rightTrace !== null && /** @type {AbstractType<any>} */ (rightTrace.parent)._item === parentItem) {
right = rightTrace
break
}
@ -228,7 +227,7 @@ export const redoItem = (transaction, item, redoitems) => {
)
item.redone = nextId
keepItem(redoneItem, true)
redoneItem.integrate(transaction)
redoneItem.integrate(transaction, 0)
return redoneItem
}
@ -242,7 +241,7 @@ export class Item extends AbstractStruct {
* @param {ID | null} origin
* @param {Item | null} right
* @param {ID | null} rightOrigin
* @param {AbstractType<any>} parent
* @param {AbstractType<any>|ID|null} parent Is a type if integrated, is null if it is possible to copy parent from left or right, is ID before integration to search for it.
* @param {string | null} parentSub
* @param {AbstractContent} content
*/
@ -251,7 +250,6 @@ export class Item extends AbstractStruct {
/**
* The item that was originally to the left of this item.
* @type {ID | null}
* @readonly
*/
this.origin = origin
/**
@ -266,14 +264,11 @@ export class Item extends AbstractStruct {
this.right = right
/**
* The item that was originally to the right of this item.
* @readonly
* @type {ID | null}
*/
this.rightOrigin = rightOrigin
/**
* The parent type.
* @type {AbstractType<any>}
* @readonly
* @type {AbstractType<any>|ID|null}
*/
this.parent = parent
/**
@ -282,7 +277,6 @@ export class Item extends AbstractStruct {
* to insert this item. If `parentSub = null` type._start is the list in
* which to insert to. Otherwise it is `parent._map`.
* @type {String | null}
* @readonly
*/
this.parentSub = parentSub
/**
@ -311,17 +305,86 @@ export class Item extends AbstractStruct {
}
/**
* Return missing ids, or define missing items and return null.
*
* @param {Transaction} transaction
* @param {StructStore} store
* @return {null | ID}
*/
integrate (transaction) {
getMissing (transaction, store) {
const origin = this.origin
const rightOrigin = this.rightOrigin
const parent = /** @type {ID} */ (this.parent)
if (origin && origin.clock >= getState(store, origin.client)) {
return this.origin
}
if (rightOrigin && rightOrigin.clock >= getState(store, rightOrigin.client)) {
return this.rightOrigin
}
if (parent && parent.constructor === ID && parent.clock >= getState(store, parent.client)) {
return parent
}
// We have all missing ids, now find the items
if (origin) {
this.left = getItemCleanEnd(transaction, store, origin)
this.origin = this.left.lastId
}
if (rightOrigin) {
this.right = getItemCleanStart(transaction, rightOrigin)
this.rightOrigin = this.right.id
}
if (parent && parent.constructor === ID) {
if (parent.clock < getState(store, parent.client)) {
const parentItem = getItem(store, parent)
if (parentItem.constructor === GC) {
this.parent = null
} else {
this.parent = /** @type {ContentType} */ (parentItem.content).type
}
} else {
return parent
}
}
// only set item if this shouldn't be garbage collected
if (!this.parent) {
if (this.left && this.left.constructor === Item) {
this.parent = this.left.parent
this.parentSub = this.left.parentSub
}
if (this.right && this.right.constructor === Item) {
this.parent = this.right.parent
this.parentSub = this.right.parentSub
}
}
return null
}
/**
* @param {Transaction} transaction
* @param {number} offset
*/
integrate (transaction, offset) {
const store = transaction.doc.store
const parent = this.parent
if (offset > 0) {
this.id.clock += offset
this.left = getItemCleanEnd(transaction, store, createID(this.id.client, this.id.clock - 1))
this.origin = this.left.lastId
this.content = this.content.splice(offset)
this.length -= offset
}
const parentSub = this.parentSub
const length = this.length
const parent = /** @type {AbstractType<any>|null} */ (this.parent)
if (parent) {
/**
* @type {Item|null}
*/
let left = this.left
/**
* @type {Item|null}
*/
@ -338,6 +401,7 @@ export class Item extends AbstractStruct {
o = parent._start
}
// TODO: use something like DeleteSet here (a tree implementation would be best)
// @todo use global set definitions
/**
* @type {Set<Item>}
*/
@ -410,6 +474,10 @@ export class Item extends AbstractStruct {
// delete if parent is deleted or if this is not the current attribute value of parent
this.delete(transaction)
}
} else {
// parent is not defined. Integrate GC struct instead
new GC(this.id, this.length).integrate(transaction, 0)
}
}
/**
@ -481,7 +549,7 @@ export class Item extends AbstractStruct {
*/
delete (transaction) {
if (!this.deleted) {
const parent = this.parent
const parent = /** @type {AbstractType<any>} */ (this.parent)
// adjust the length of parent
if (this.countable && this.parentSub === null) {
parent._length -= this.length
@ -534,7 +602,7 @@ export class Item extends AbstractStruct {
writeID(encoder, rightOrigin)
}
if (origin === null && rightOrigin === null) {
const parent = this.parent
const parent = /** @type {AbstractType<any>} */ (this.parent)
const parentItem = parent._item
if (parentItem === null) {
// parent type on y._map
@ -669,27 +737,23 @@ export class AbstractContent {
}
}
/**
* @private
*/
export class ItemRef extends AbstractStructRef {
/**
* @param {decoding.Decoder} decoder
* @param {ID} id
* @param {number} info
* @param {Doc} doc
*/
constructor (decoder, id, info) {
super(id)
export const readItem = (decoder, id, info, doc) => {
/**
* The item that was originally to the left of this item.
* @type {ID | null}
*/
this.left = (info & binary.BIT8) === binary.BIT8 ? readID(decoder) : null
const origin = (info & binary.BIT8) === binary.BIT8 ? readID(decoder) : null
/**
* The item that was originally to the right of this item.
* @type {ID | null}
*/
this.right = (info & binary.BIT7) === binary.BIT7 ? readID(decoder) : null
const rightOrigin = (info & binary.BIT7) === binary.BIT7 ? readID(decoder) : null
const canCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0
const hasParentYKey = canCopyParentInfo ? decoding.readVarUint(decoder) === 1 : false
/**
@ -698,12 +762,12 @@ export class ItemRef extends AbstractStructRef {
* It indicates how we store/retrieve parent from `y.share`
* @type {string|null}
*/
this.parentYKey = canCopyParentInfo && hasParentYKey ? decoding.readVarString(decoder) : null
const parentYKey = canCopyParentInfo && hasParentYKey ? decoding.readVarString(decoder) : null
/**
* The parent type.
* @type {ID | null}
* @type {ID | AbstractType<any> | null}
*/
this.parent = canCopyParentInfo && !hasParentYKey ? readID(decoder) : null
const parent = canCopyParentInfo && !hasParentYKey ? readID(decoder) : (parentYKey ? doc.get(parentYKey) : null)
/**
* If the parent refers to this item with some kind of key (e.g. YMap, the
* key is specified here. The key is then used to refer to the list in which
@ -711,81 +775,12 @@ export class ItemRef extends AbstractStructRef {
* which to insert to. Otherwise it is `parent._map`.
* @type {String | null}
*/
this.parentSub = canCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoding.readVarString(decoder) : null
const missing = this._missing
// Only add items to missing if they don't preceed this item (indicating that it has already been added).
// @todo Creating missing items could be done outside this constructor
if (this.left !== null && this.left.client !== id.client) {
missing.push(this.left)
}
if (this.right !== null && this.right.client !== id.client) {
missing.push(this.right)
}
if (this.parent !== null) {
missing.push(this.parent)
}
const parentSub = canCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoding.readVarString(decoder) : null
/**
* @type {AbstractContent}
*/
this.content = readItemContent(decoder, info)
this.length = this.content.getLength()
}
const content = readItemContent(decoder, info)
/**
* @param {Transaction} transaction
* @param {StructStore} store
* @param {number} offset
* @return {Item|GC}
*/
toStruct (transaction, store, offset) {
if (offset > 0) {
this.id.clock += offset
this.left = createID(this.id.client, this.id.clock - 1)
this.content = this.content.splice(offset)
this.length -= offset
}
const left = this.left === null ? null : getItemCleanEnd(transaction, store, this.left)
const right = this.right === null ? null : getItemCleanStart(transaction, this.right)
const parentId = this.parent
let parent = null
let parentSub = this.parentSub
if (parentId !== null) {
const parentItem = getItem(store, parentId)
// Edge case: toStruct is called with an offset > 0. In this case left is defined.
// Depending in which order structs arrive, left may be GC'd and the parent not
// deleted. This is why we check if left is GC'd. Strictly we don't have
// to check if right is GC'd, but we will in case we run into future issues
if (!parentItem.deleted && (left === null || left.constructor !== GC) && (right === null || right.constructor !== GC)) {
parent = /** @type {ContentType} */ (parentItem.content).type
}
} else if (this.parentYKey !== null) {
parent = transaction.doc.get(this.parentYKey)
} else if (left !== null) {
if (left.constructor !== GC) {
parent = left.parent
parentSub = left.parentSub
}
} else if (right !== null) {
if (right.constructor !== GC) {
parent = right.parent
parentSub = right.parentSub
}
} else {
throw error.unexpectedCase()
}
return parent === null
? new GC(this.id, this.length)
: new Item(
this.id,
left,
left && left.lastId,
right,
right && right.id,
parent,
parentSub,
this.content
)
}
return new Item(id, null, origin, null, rightOrigin, parent, parentSub, content)
}

View File

@ -53,7 +53,7 @@ export const callTypeObservers = (type, transaction, event) => {
if (type._item === null) {
break
}
type = type._item.parent
type = /** @type {AbstractType<any>} */ (type._item.parent)
}
callEventHandlerListeners(changedType._eH, event, transaction)
}
@ -386,7 +386,7 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
const packJsonContent = () => {
if (jsonContent.length > 0) {
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentAny(jsonContent))
left.integrate(transaction)
left.integrate(transaction, 0)
jsonContent = []
}
}
@ -405,12 +405,12 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
case Uint8Array:
case ArrayBuffer:
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c))))
left.integrate(transaction)
left.integrate(transaction, 0)
break
default:
if (c instanceof AbstractType) {
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentType(c))
left.integrate(transaction)
left.integrate(transaction, 0)
} else {
throw new Error('Unexpected content type in insert operation')
}
@ -537,7 +537,7 @@ export const typeMapSet = (transaction, parent, key, value) => {
}
}
}
new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, null, null, parent, key, content).integrate(transaction)
new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, null, null, parent, key, content).integrate(transaction, 0)
}
/**

View File

@ -153,7 +153,7 @@ const insertNegatedAttributes = (transaction, parent, currPos, negatedAttributes
const ownClientId = doc.clientID
for (const [key, val] of negatedAttributes) {
left = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
left.integrate(transaction)
left.integrate(transaction, 0)
}
currPos.left = left
currPos.right = right
@ -228,7 +228,7 @@ const insertAttributes = (transaction, parent, currPos, currentAttributes, attri
negatedAttributes.set(key, currentVal)
const { left, right } = currPos
currPos.left = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
currPos.left.integrate(transaction)
currPos.left.integrate(transaction, 0)
}
}
return negatedAttributes
@ -259,7 +259,7 @@ const insertText = (transaction, parent, currPos, currentAttributes, text, attri
const content = text.constructor === String ? new ContentString(/** @type {string} */ (text)) : new ContentEmbed(text)
const { left, right } = currPos
currPos.left = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, content)
currPos.left.integrate(transaction)
currPos.left.integrate(transaction, 0)
return insertNegatedAttributes(transaction, parent, currPos, negatedAttributes)
}
@ -320,7 +320,7 @@ const formatText = (transaction, parent, currPos, currentAttributes, length, att
newlines += '\n'
}
left = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentString(newlines))
left.integrate(transaction)
left.integrate(transaction, 0)
}
currPos.left = left
currPos.right = right

View File

@ -97,7 +97,7 @@ export class YXmlTreeWalker {
} else if (n.parent === this._root) {
n = null
} else {
n = n.parent._item
n = /** @type {AbstractType<any>} */ (n.parent)._item
}
}
}

View File

@ -227,7 +227,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
if (!(right instanceof Item)) {
return null
}
type = right.parent
type = /** @type {AbstractType<any>} */ (right.parent)
if (type._item === null || !type._item.deleted) {
index = right.deleted || !right.countable ? 0 : res.diff
let n = right.left

View File

@ -2,7 +2,7 @@
import {
GC,
splitItem,
AbstractStruct, GCRef, ItemRef, Transaction, ID, Item // eslint-disable-line
AbstractStruct, Transaction, ID, Item // eslint-disable-line
} from '../internals.js'
import * as math from 'lib0/math.js'
@ -21,13 +21,13 @@ export class StructStore {
* We could shift the array of refs instead, but shift is incredible
* slow in Chrome for arrays with more than 100k elements
* @see tryResumePendingStructRefs
* @type {Map<number,{i:number,refs:Array<GCRef|ItemRef>}>}
* @type {Map<number,{i:number,refs:Array<GC|Item>}>}
*/
this.pendingClientsStructRefs = new Map()
/**
* Stack of pending structs waiting for struct dependencies
* Maximum length of stack is structReaders.size
* @type {Array<GCRef|ItemRef>}
* @type {Array<GC|Item>}
*/
this.pendingStack = []
/**
@ -129,7 +129,10 @@ export const findIndexSS = (structs, clock) => {
if (mid.id.clock === clock) {
return right
}
let midindex = math.floor((clock / (midclock + mid.length)) * right) // pivoting the search
// @todo does it even make sense to pivot the search?
// If a good split misses, it might actually increase the time to find the correct item.
// Currently, the only advantage is that search with pivoting might find the item on the first try.
let midindex = math.floor((clock / (midclock + mid.length - 1)) * right) // pivoting the search
while (left <= right) {
mid = structs[midindex]
midclock = mid.id.clock

View File

@ -156,8 +156,8 @@ const tryToMergeWithLeft = (structs, pos) => {
if (left.deleted === right.deleted && left.constructor === right.constructor) {
if (left.mergeWith(right)) {
structs.splice(pos, 1)
if (right instanceof Item && right.parentSub !== null && right.parent._map.get(right.parentSub) === right) {
right.parent._map.set(right.parentSub, /** @type {Item} */ (left))
if (right instanceof Item && right.parentSub !== null && /** @type {AbstractType<any>} */ (right.parent)._map.get(right.parentSub) === right) {
/** @type {AbstractType<any>} */ (right.parent)._map.set(right.parentSub, /** @type {Item} */ (left))
}
}
}

View File

@ -211,7 +211,7 @@ const getPathTo = (parent, child) => {
} else {
// parent is array-ish
let i = 0
let c = child._item.parent._start
let c = /** @type {AbstractType<any>} */ (child._item.parent)._start
while (c !== child._item && c !== null) {
if (!c.deleted) {
i++
@ -220,7 +220,7 @@ const getPathTo = (parent, child) => {
}
path.unshift(i)
}
child = child._item.parent
child = /** @type {AbstractType<any>} */ (child._item.parent)
}
return path
}

View File

@ -16,8 +16,6 @@
import {
findIndexSS,
GCRef,
ItemRef,
writeID,
readID,
getState,
@ -27,6 +25,7 @@ import {
writeDeleteSet,
createDeleteSetFromStructStore,
transact,
readItem,
Doc, Transaction, GC, Item, StructStore, ID // eslint-disable-line
} from '../internals.js'
@ -80,7 +79,9 @@ export const writeClientsStructs = (encoder, store, _sm) => {
})
// write # states that were updated
encoding.writeVarUint(encoder, sm.size)
sm.forEach((clock, client) => {
// Write items with higher client ids first
// This heavily improves the conflict algorithm.
Array.from(sm.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
// @ts-ignore
writeStructs(encoder, store.clients.get(client), client, clock)
})
@ -88,13 +89,14 @@ export const writeClientsStructs = (encoder, store, _sm) => {
/**
* @param {decoding.Decoder} decoder The decoder object to read data from.
* @param {Map<number,Array<GCRef|ItemRef>>} clientRefs
* @return {Map<number,Array<GCRef|ItemRef>>}
* @param {Map<number,Array<GC|Item>>} clientRefs
* @param {Doc} doc
* @return {Map<number,Array<GC|Item>>}
*
* @private
* @function
*/
export const readClientsStructRefs = (decoder, clientRefs) => {
export const readClientsStructRefs = (decoder, clientRefs, doc) => {
const numOfStateUpdates = decoding.readVarUint(decoder)
for (let i = 0; i < numOfStateUpdates; i++) {
const numberOfStructs = decoding.readVarUint(decoder)
@ -102,15 +104,16 @@ export const readClientsStructRefs = (decoder, clientRefs) => {
const nextIdClient = nextID.client
let nextIdClock = nextID.clock
/**
* @type {Array<GCRef|ItemRef>}
* @type {Array<GC|Item>}
*/
const refs = []
clientRefs.set(nextIdClient, refs)
for (let i = 0; i < numberOfStructs; i++) {
const info = decoding.readUint8(decoder)
const ref = (binary.BITS5 & info) === 0 ? new GCRef(decoder, createID(nextIdClient, nextIdClock), info) : new ItemRef(decoder, createID(nextIdClient, nextIdClock), info)
refs.push(ref)
nextIdClock += ref.length
const id = createID(nextIdClient, nextIdClock)
const struct = (binary.BITS5 & info) === 0 ? new GC(id, decoding.readVarUint(decoder)) : readItem(decoder, id, info, doc)
refs.push(struct)
nextIdClock += struct.length
}
}
return clientRefs
@ -155,12 +158,12 @@ const resumeStructIntegration = (transaction, store) => {
}
}
const ref = stack[stack.length - 1]
const m = ref._missing
const refID = ref.id
const client = refID.client
const refClock = refID.clock
const localClock = getState(store, client)
const offset = refClock < localClock ? localClock - refClock : 0
const missing = ref.getMissing(transaction, store)
if (refClock + offset !== localClock) {
// A previous message from this client is missing
// check if there is a pending structRef with a smaller clock and switch them
@ -180,9 +183,7 @@ const resumeStructIntegration = (transaction, store) => {
// wait until missing struct is available
return
}
while (m.length > 0) {
const missing = m[m.length - 1]
if (getState(store, missing.client) <= missing.clock) {
if (missing) {
const client = missing.client
// get the struct reader that has the missing struct
const structRefs = clientsStructRefs.get(client)
@ -194,13 +195,9 @@ const resumeStructIntegration = (transaction, store) => {
if (structRefs.i === structRefs.refs.length) {
clientsStructRefs.delete(client)
}
break
}
ref._missing.pop()
}
if (m.length === 0) {
} else {
if (offset < ref.length) {
ref.toStruct(transaction, store, offset).integrate(transaction)
ref.integrate(transaction, offset)
}
stack.pop()
}
@ -233,7 +230,7 @@ export const writeStructsFromTransaction = (encoder, transaction) => writeClient
/**
* @param {StructStore} store
* @param {Map<number, Array<GCRef|ItemRef>>} clientsStructsRefs
* @param {Map<number, Array<GC|Item>>} clientsStructsRefs
*
* @private
* @function
@ -270,7 +267,7 @@ const mergeReadStructsIntoPendingReads = (store, clientsStructsRefs) => {
*/
export const readStructs = (decoder, transaction, store) => {
const clientsStructRefs = new Map()
readClientsStructRefs(decoder, clientsStructRefs)
readClientsStructRefs(decoder, clientsStructRefs, transaction.doc)
mergeReadStructsIntoPendingReads(store, clientsStructRefs)
resumeStructIntegration(transaction, store)
tryResumePendingDeleteReaders(transaction, store)

View File

@ -16,7 +16,7 @@ export const isParentOf = (parent, child) => {
if (child.parent === parent) {
return true
}
child = child.parent._item
child = /** @type {AbstractType<any>} */ (child.parent)._item
}
return false
}

View File

@ -454,7 +454,7 @@ const mapTransactions = [
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYmapTests10 = tc => {
applyRandomTests(tc, mapTransactions, 10)
applyRandomTests(tc, mapTransactions, 3)
}
/**

View File

@ -209,16 +209,28 @@ export const testFormattingRemovedInMidText = tc => {
* @param {t.TestCase} tc
*/
export const testLargeFragmentedDocument = tc => {
const { text0, testConnector } = init(tc, { users: 2 })
// @ts-ignore
text0.doc.transact(() => {
for (let i = 0; i < 1000000; i++) {
const itemsToInsert = 1000000
let update = /** @type {any} */ (null)
;(() => {
const doc1 = new Y.Doc()
const text0 = doc1.getText('txt')
t.measureTime(`time to insert ${itemsToInsert}`, () => {
doc1.transact(() => {
for (let i = 0; i < itemsToInsert; i++) {
text0.insert(0, '0')
}
})
t.measureTime('time to apply', () => {
testConnector.flushAllMessages()
})
t.measureTime('time to encode', () => {
update = Y.encodeStateAsUpdate(doc1)
})
})()
;(() => {
const doc2 = new Y.Doc()
t.measureTime('time to apply', () => {
Y.applyUpdate(doc2, update)
})
})()
}
// RANDOM TESTS