Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fb63de8fc | ||
|
|
c4d80d133d | ||
|
|
cebe96c001 | ||
|
|
4d2369ce21 | ||
|
|
5293ab4df1 | ||
|
|
e53c01c6c5 | ||
|
|
03faa27787 | ||
|
|
868dd5f0a5 | ||
|
|
fa58ce53cd | ||
|
|
0a0098fdfb | ||
|
|
a5a48d07f6 | ||
|
|
7b16d5c92d | ||
|
|
ee147c14f1 | ||
|
|
e86d5ba25b | ||
|
|
149ca6f636 | ||
|
|
e4223760b0 | ||
|
|
9d3dd4e082 | ||
|
|
5a4ff33bf4 | ||
|
|
469404c6e1 |
@@ -40,6 +40,7 @@ height="60px" />](https://input.com/)
|
|||||||
[](https://github.com/journeyapps)
|
[](https://github.com/journeyapps)
|
||||||
[](https://github.com/adabru)
|
[](https://github.com/adabru)
|
||||||
[](https://github.com/NathanaelA)
|
[](https://github.com/NathanaelA)
|
||||||
|
[](https://github.com/gremloon)
|
||||||
|
|
||||||
Sponsorship also comes with special perks! [](https://github.com/sponsors/dmonad)
|
Sponsorship also comes with special perks! [](https://github.com/sponsors/dmonad)
|
||||||
|
|
||||||
@@ -395,8 +396,12 @@ YTextEvents compute changes as deltas.
|
|||||||
<dd></dd>
|
<dd></dd>
|
||||||
<b><code>format(index:number, length:number, formattingAttributes:Object<string,string>)</code></b>
|
<b><code>format(index:number, length:number, formattingAttributes:Object<string,string>)</code></b>
|
||||||
<dd>Assign formatting attributes to a range in the text</dd>
|
<dd>Assign formatting attributes to a range in the text</dd>
|
||||||
<b><code>applyDelta(delta)</code></b>
|
<b><code>applyDelta(delta, opts:Object<string,any>)</code></b>
|
||||||
<dd>See <a href="https://quilljs.com/docs/delta/">Quill Delta</a></dd>
|
<dd>
|
||||||
|
See <a href="https://quilljs.com/docs/delta/">Quill Delta</a>
|
||||||
|
Can set options for preventing remove ending newLines, default is true.
|
||||||
|
<pre>ytext.applyDelta(delta, { sanitize: false })</pre>
|
||||||
|
</dd>
|
||||||
<b><code>length:number</code></b>
|
<b><code>length:number</code></b>
|
||||||
<dd></dd>
|
<dd></dd>
|
||||||
<b><code>toString():string</code></b>
|
<b><code>toString():string</code></b>
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.1.0",
|
"version": "13.2.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.1.0",
|
"version": "13.2.0",
|
||||||
"description": "Shared Editing Library",
|
"description": "Shared Editing Library",
|
||||||
"main": "./dist/yjs.cjs",
|
"main": "./dist/yjs.cjs",
|
||||||
"module": "./dist/yjs.mjs",
|
"module": "./dist/yjs.mjs",
|
||||||
|
|||||||
@@ -39,39 +39,9 @@ export class AbstractStruct {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Transaction} transaction
|
* @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
|
* @param {number} offset
|
||||||
* @return {AbstractStruct}
|
|
||||||
*/
|
*/
|
||||||
toStruct (transaction, store, offset) {
|
integrate (transaction, offset) {
|
||||||
throw error.methodUnimplemented()
|
throw error.methodUnimplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
AbstractStructRef,
|
|
||||||
AbstractStruct,
|
AbstractStruct,
|
||||||
addStruct,
|
addStruct,
|
||||||
StructStore, Transaction, ID // eslint-disable-line
|
StructStore, Transaction, ID // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as decoding from 'lib0/decoding.js'
|
|
||||||
import * as encoding from 'lib0/encoding.js'
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
|
||||||
export const structGCRefNumber = 0
|
export const structGCRefNumber = 0
|
||||||
@@ -37,8 +35,13 @@ export class GC extends AbstractStruct {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Transaction} transaction
|
* @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)
|
addStruct(transaction.doc.store, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,39 +53,13 @@ export class GC extends AbstractStruct {
|
|||||||
encoding.writeUint8(encoder, structGCRefNumber)
|
encoding.writeUint8(encoder, structGCRefNumber)
|
||||||
encoding.writeVarUint(encoder, this.length - offset)
|
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 {Transaction} transaction
|
||||||
* @param {StructStore} store
|
* @param {StructStore} store
|
||||||
* @param {number} offset
|
* @return {null | ID}
|
||||||
* @return {GC}
|
|
||||||
*/
|
*/
|
||||||
toStruct (transaction, store, offset) {
|
getMissing (transaction, store) {
|
||||||
if (offset > 0) {
|
return null
|
||||||
this.id.clock += offset
|
|
||||||
this.length -= offset
|
|
||||||
}
|
|
||||||
return new GC(
|
|
||||||
this.id,
|
|
||||||
this.length
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
writeID,
|
writeID,
|
||||||
GC,
|
GC,
|
||||||
getState,
|
getState,
|
||||||
AbstractStructRef,
|
|
||||||
AbstractStruct,
|
AbstractStruct,
|
||||||
replaceStruct,
|
replaceStruct,
|
||||||
addStruct,
|
addStruct,
|
||||||
@@ -24,7 +23,7 @@ import {
|
|||||||
readContentFormat,
|
readContentFormat,
|
||||||
readContentType,
|
readContentType,
|
||||||
addChangedTypeToTransaction,
|
addChangedTypeToTransaction,
|
||||||
ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line
|
Doc, ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as error from 'lib0/error.js'
|
import * as error from 'lib0/error.js'
|
||||||
@@ -73,7 +72,7 @@ export const followRedone = (store, id) => {
|
|||||||
export const keepItem = (item, keep) => {
|
export const keepItem = (item, keep) => {
|
||||||
while (item !== null && item.keep !== keep) {
|
while (item !== null && item.keep !== keep) {
|
||||||
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)
|
transaction._mergeStructs.push(rightItem)
|
||||||
// update parent._map
|
// update parent._map
|
||||||
if (rightItem.parentSub !== null && rightItem.right === null) {
|
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
|
leftItem.length = diff
|
||||||
return rightItem
|
return rightItem
|
||||||
@@ -144,7 +143,7 @@ export const redoItem = (transaction, item, redoitems) => {
|
|||||||
if (redone !== null) {
|
if (redone !== null) {
|
||||||
return getItemCleanStart(transaction, redone)
|
return getItemCleanStart(transaction, redone)
|
||||||
}
|
}
|
||||||
let parentItem = item.parent._item
|
let parentItem = /** @type {AbstractType<any>} */ (item.parent)._item
|
||||||
/**
|
/**
|
||||||
* @type {Item|null}
|
* @type {Item|null}
|
||||||
*/
|
*/
|
||||||
@@ -169,7 +168,7 @@ export const redoItem = (transaction, item, redoitems) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (left.right !== null) {
|
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
|
right = null
|
||||||
}
|
}
|
||||||
@@ -191,10 +190,10 @@ export const redoItem = (transaction, item, redoitems) => {
|
|||||||
*/
|
*/
|
||||||
let leftTrace = left
|
let leftTrace = left
|
||||||
// trace redone until parent matches
|
// 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)
|
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
|
left = leftTrace
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -206,10 +205,10 @@ export const redoItem = (transaction, item, redoitems) => {
|
|||||||
*/
|
*/
|
||||||
let rightTrace = right
|
let rightTrace = right
|
||||||
// trace redone until parent matches
|
// 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)
|
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
|
right = rightTrace
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -228,7 +227,7 @@ export const redoItem = (transaction, item, redoitems) => {
|
|||||||
)
|
)
|
||||||
item.redone = nextId
|
item.redone = nextId
|
||||||
keepItem(redoneItem, true)
|
keepItem(redoneItem, true)
|
||||||
redoneItem.integrate(transaction)
|
redoneItem.integrate(transaction, 0)
|
||||||
return redoneItem
|
return redoneItem
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +241,7 @@ export class Item extends AbstractStruct {
|
|||||||
* @param {ID | null} origin
|
* @param {ID | null} origin
|
||||||
* @param {Item | null} right
|
* @param {Item | null} right
|
||||||
* @param {ID | null} rightOrigin
|
* @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 {string | null} parentSub
|
||||||
* @param {AbstractContent} content
|
* @param {AbstractContent} content
|
||||||
*/
|
*/
|
||||||
@@ -251,7 +250,6 @@ export class Item extends AbstractStruct {
|
|||||||
/**
|
/**
|
||||||
* The item that was originally to the left of this item.
|
* The item that was originally to the left of this item.
|
||||||
* @type {ID | null}
|
* @type {ID | null}
|
||||||
* @readonly
|
|
||||||
*/
|
*/
|
||||||
this.origin = origin
|
this.origin = origin
|
||||||
/**
|
/**
|
||||||
@@ -266,14 +264,11 @@ export class Item extends AbstractStruct {
|
|||||||
this.right = right
|
this.right = right
|
||||||
/**
|
/**
|
||||||
* The item that was originally to the right of this item.
|
* The item that was originally to the right of this item.
|
||||||
* @readonly
|
|
||||||
* @type {ID | null}
|
* @type {ID | null}
|
||||||
*/
|
*/
|
||||||
this.rightOrigin = rightOrigin
|
this.rightOrigin = rightOrigin
|
||||||
/**
|
/**
|
||||||
* The parent type.
|
* @type {AbstractType<any>|ID|null}
|
||||||
* @type {AbstractType<any>}
|
|
||||||
* @readonly
|
|
||||||
*/
|
*/
|
||||||
this.parent = parent
|
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
|
* to insert this item. If `parentSub = null` type._start is the list in
|
||||||
* which to insert to. Otherwise it is `parent._map`.
|
* which to insert to. Otherwise it is `parent._map`.
|
||||||
* @type {String | null}
|
* @type {String | null}
|
||||||
* @readonly
|
|
||||||
*/
|
*/
|
||||||
this.parentSub = parentSub
|
this.parentSub = parentSub
|
||||||
/**
|
/**
|
||||||
@@ -311,104 +305,178 @@ export class Item extends AbstractStruct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Return missing ids, or define missing items and return null.
|
||||||
|
*
|
||||||
* @param {Transaction} transaction
|
* @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 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 parentSub = this.parentSub
|
||||||
const length = this.length
|
const length = this.length
|
||||||
/**
|
const parent = /** @type {AbstractType<any>|null} */ (this.parent)
|
||||||
* @type {Item|null}
|
|
||||||
*/
|
if (parent) {
|
||||||
let left = this.left
|
/**
|
||||||
/**
|
* @type {Item|null}
|
||||||
* @type {Item|null}
|
*/
|
||||||
*/
|
let left = this.left
|
||||||
let o
|
|
||||||
// set o to the first conflicting item
|
/**
|
||||||
if (left !== null) {
|
* @type {Item|null}
|
||||||
o = left.right
|
*/
|
||||||
} else if (parentSub !== null) {
|
let o
|
||||||
o = parent._map.get(parentSub) || null
|
// set o to the first conflicting item
|
||||||
while (o !== null && o.left !== null) {
|
|
||||||
o = o.left
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
o = parent._start
|
|
||||||
}
|
|
||||||
// TODO: use something like DeleteSet here (a tree implementation would be best)
|
|
||||||
/**
|
|
||||||
* @type {Set<Item>}
|
|
||||||
*/
|
|
||||||
const conflictingItems = new Set()
|
|
||||||
/**
|
|
||||||
* @type {Set<Item>}
|
|
||||||
*/
|
|
||||||
const itemsBeforeOrigin = new Set()
|
|
||||||
// Let c in conflictingItems, b in itemsBeforeOrigin
|
|
||||||
// ***{origin}bbbb{this}{c,b}{c,b}{o}***
|
|
||||||
// Note that conflictingItems is a subset of itemsBeforeOrigin
|
|
||||||
while (o !== null && o !== this.right) {
|
|
||||||
itemsBeforeOrigin.add(o)
|
|
||||||
conflictingItems.add(o)
|
|
||||||
if (compareIDs(this.origin, o.origin)) {
|
|
||||||
// case 1
|
|
||||||
if (o.id.client < this.id.client) {
|
|
||||||
left = o
|
|
||||||
conflictingItems.clear()
|
|
||||||
}
|
|
||||||
} else if (o.origin !== null && itemsBeforeOrigin.has(getItem(store, o.origin))) {
|
|
||||||
// case 2
|
|
||||||
if (o.origin === null || !conflictingItems.has(getItem(store, o.origin))) {
|
|
||||||
left = o
|
|
||||||
conflictingItems.clear()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
o = o.right
|
|
||||||
}
|
|
||||||
this.left = left
|
|
||||||
// reconnect left/right + update parent map/start if necessary
|
|
||||||
if (left !== null) {
|
|
||||||
const right = left.right
|
|
||||||
this.right = right
|
|
||||||
left.right = this
|
|
||||||
} else {
|
|
||||||
let r
|
|
||||||
if (parentSub !== null) {
|
|
||||||
r = parent._map.get(parentSub) || null
|
|
||||||
while (r !== null && r.left !== null) {
|
|
||||||
r = r.left
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
r = parent._start
|
|
||||||
parent._start = this
|
|
||||||
}
|
|
||||||
this.right = r
|
|
||||||
}
|
|
||||||
if (this.right !== null) {
|
|
||||||
this.right.left = this
|
|
||||||
} else if (parentSub !== null) {
|
|
||||||
// set as current parent value if right === null and this is parentSub
|
|
||||||
parent._map.set(parentSub, this)
|
|
||||||
if (left !== null) {
|
if (left !== null) {
|
||||||
// this is the current attribute value of parent. delete right
|
o = left.right
|
||||||
left.delete(transaction)
|
} else if (parentSub !== null) {
|
||||||
|
o = parent._map.get(parentSub) || null
|
||||||
|
while (o !== null && o.left !== null) {
|
||||||
|
o = o.left
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
o = parent._start
|
||||||
}
|
}
|
||||||
}
|
// TODO: use something like DeleteSet here (a tree implementation would be best)
|
||||||
// adjust length of parent
|
// @todo use global set definitions
|
||||||
if (parentSub === null && this.countable && !this.deleted) {
|
/**
|
||||||
parent._length += length
|
* @type {Set<Item>}
|
||||||
}
|
*/
|
||||||
addStruct(store, this)
|
const conflictingItems = new Set()
|
||||||
this.content.integrate(transaction, this)
|
/**
|
||||||
// add parent to transaction.changed
|
* @type {Set<Item>}
|
||||||
addChangedTypeToTransaction(transaction, parent, parentSub)
|
*/
|
||||||
if ((parent._item !== null && parent._item.deleted) || (this.right !== null && parentSub !== null)) {
|
const itemsBeforeOrigin = new Set()
|
||||||
// delete if parent is deleted or if this is not the current attribute value of parent
|
// Let c in conflictingItems, b in itemsBeforeOrigin
|
||||||
this.delete(transaction)
|
// ***{origin}bbbb{this}{c,b}{c,b}{o}***
|
||||||
|
// Note that conflictingItems is a subset of itemsBeforeOrigin
|
||||||
|
while (o !== null && o !== this.right) {
|
||||||
|
itemsBeforeOrigin.add(o)
|
||||||
|
conflictingItems.add(o)
|
||||||
|
if (compareIDs(this.origin, o.origin)) {
|
||||||
|
// case 1
|
||||||
|
if (o.id.client < this.id.client) {
|
||||||
|
left = o
|
||||||
|
conflictingItems.clear()
|
||||||
|
}
|
||||||
|
} else if (o.origin !== null && itemsBeforeOrigin.has(getItem(store, o.origin))) {
|
||||||
|
// case 2
|
||||||
|
if (o.origin === null || !conflictingItems.has(getItem(store, o.origin))) {
|
||||||
|
left = o
|
||||||
|
conflictingItems.clear()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
o = o.right
|
||||||
|
}
|
||||||
|
this.left = left
|
||||||
|
// reconnect left/right + update parent map/start if necessary
|
||||||
|
if (left !== null) {
|
||||||
|
const right = left.right
|
||||||
|
this.right = right
|
||||||
|
left.right = this
|
||||||
|
} else {
|
||||||
|
let r
|
||||||
|
if (parentSub !== null) {
|
||||||
|
r = parent._map.get(parentSub) || null
|
||||||
|
while (r !== null && r.left !== null) {
|
||||||
|
r = r.left
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r = parent._start
|
||||||
|
parent._start = this
|
||||||
|
}
|
||||||
|
this.right = r
|
||||||
|
}
|
||||||
|
if (this.right !== null) {
|
||||||
|
this.right.left = this
|
||||||
|
} else if (parentSub !== null) {
|
||||||
|
// set as current parent value if right === null and this is parentSub
|
||||||
|
parent._map.set(parentSub, this)
|
||||||
|
if (left !== null) {
|
||||||
|
// this is the current attribute value of parent. delete right
|
||||||
|
left.delete(transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// adjust length of parent
|
||||||
|
if (parentSub === null && this.countable && !this.deleted) {
|
||||||
|
parent._length += length
|
||||||
|
}
|
||||||
|
addStruct(store, this)
|
||||||
|
this.content.integrate(transaction, this)
|
||||||
|
// add parent to transaction.changed
|
||||||
|
addChangedTypeToTransaction(transaction, parent, parentSub)
|
||||||
|
if ((parent._item !== null && parent._item.deleted) || (this.right !== null && parentSub !== null)) {
|
||||||
|
// 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,7 +506,8 @@ export class Item extends AbstractStruct {
|
|||||||
* Computes the last content address of this Item.
|
* Computes the last content address of this Item.
|
||||||
*/
|
*/
|
||||||
get lastId () {
|
get lastId () {
|
||||||
return createID(this.id.client, this.id.clock + this.length - 1)
|
// allocating ids is pretty costly because of the amount of ids created, so we try to reuse whenever possible
|
||||||
|
return this.length === 1 ? this.id : createID(this.id.client, this.id.clock + this.length - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -480,7 +549,7 @@ export class Item extends AbstractStruct {
|
|||||||
*/
|
*/
|
||||||
delete (transaction) {
|
delete (transaction) {
|
||||||
if (!this.deleted) {
|
if (!this.deleted) {
|
||||||
const parent = this.parent
|
const parent = /** @type {AbstractType<any>} */ (this.parent)
|
||||||
// adjust the length of parent
|
// adjust the length of parent
|
||||||
if (this.countable && this.parentSub === null) {
|
if (this.countable && this.parentSub === null) {
|
||||||
parent._length -= this.length
|
parent._length -= this.length
|
||||||
@@ -533,7 +602,7 @@ export class Item extends AbstractStruct {
|
|||||||
writeID(encoder, rightOrigin)
|
writeID(encoder, rightOrigin)
|
||||||
}
|
}
|
||||||
if (origin === null && rightOrigin === null) {
|
if (origin === null && rightOrigin === null) {
|
||||||
const parent = this.parent
|
const parent = /** @type {AbstractType<any>} */ (this.parent)
|
||||||
const parentItem = parent._item
|
const parentItem = parent._item
|
||||||
if (parentItem === null) {
|
if (parentItem === null) {
|
||||||
// parent type on y._map
|
// parent type on y._map
|
||||||
@@ -669,119 +738,49 @@ export class AbstractContent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @param {ID} id
|
||||||
|
* @param {number} info
|
||||||
|
* @param {Doc} doc
|
||||||
*/
|
*/
|
||||||
export class ItemRef extends AbstractStructRef {
|
export const readItem = (decoder, id, info, doc) => {
|
||||||
/**
|
/**
|
||||||
* @param {decoding.Decoder} decoder
|
* The item that was originally to the left of this item.
|
||||||
* @param {ID} id
|
* @type {ID | null}
|
||||||
* @param {number} info
|
|
||||||
*/
|
*/
|
||||||
constructor (decoder, id, info) {
|
const origin = (info & binary.BIT8) === binary.BIT8 ? readID(decoder) : null
|
||||||
super(id)
|
/**
|
||||||
/**
|
* The item that was originally to the right of this item.
|
||||||
* The item that was originally to the left of this item.
|
* @type {ID | null}
|
||||||
* @type {ID | null}
|
*/
|
||||||
*/
|
const rightOrigin = (info & binary.BIT7) === binary.BIT7 ? readID(decoder) : null
|
||||||
this.left = (info & binary.BIT8) === binary.BIT8 ? readID(decoder) : null
|
const canCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0
|
||||||
/**
|
const hasParentYKey = canCopyParentInfo ? decoding.readVarUint(decoder) === 1 : false
|
||||||
* The item that was originally to the right of this item.
|
/**
|
||||||
* @type {ID | null}
|
* If parent = null and neither left nor right are defined, then we know that `parent` is child of `y`
|
||||||
*/
|
* and we read the next string as parentYKey.
|
||||||
this.right = (info & binary.BIT7) === binary.BIT7 ? readID(decoder) : null
|
* It indicates how we store/retrieve parent from `y.share`
|
||||||
const canCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0
|
* @type {string|null}
|
||||||
const hasParentYKey = canCopyParentInfo ? decoding.readVarUint(decoder) === 1 : false
|
*/
|
||||||
/**
|
const parentYKey = canCopyParentInfo && hasParentYKey ? decoding.readVarString(decoder) : null
|
||||||
* If parent = null and neither left nor right are defined, then we know that `parent` is child of `y`
|
/**
|
||||||
* and we read the next string as parentYKey.
|
* The parent type.
|
||||||
* It indicates how we store/retrieve parent from `y.share`
|
* @type {ID | AbstractType<any> | null}
|
||||||
* @type {string|null}
|
*/
|
||||||
*/
|
const parent = canCopyParentInfo && !hasParentYKey ? readID(decoder) : (parentYKey ? doc.get(parentYKey) : null)
|
||||||
this.parentYKey = canCopyParentInfo && hasParentYKey ? decoding.readVarString(decoder) : null
|
/**
|
||||||
/**
|
* If the parent refers to this item with some kind of key (e.g. YMap, the
|
||||||
* The parent type.
|
* key is specified here. The key is then used to refer to the list in which
|
||||||
* @type {ID | null}
|
* to insert this item. If `parentSub = null` type._start is the list in
|
||||||
*/
|
* which to insert to. Otherwise it is `parent._map`.
|
||||||
this.parent = canCopyParentInfo && !hasParentYKey ? readID(decoder) : null
|
* @type {String | null}
|
||||||
/**
|
*/
|
||||||
* If the parent refers to this item with some kind of key (e.g. YMap, the
|
const parentSub = canCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoding.readVarString(decoder) : null
|
||||||
* key is specified here. The key is then used to refer to the list in which
|
|
||||||
* 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}
|
|
||||||
*/
|
|
||||||
this.parentSub = canCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoding.readVarString(decoder) : null
|
|
||||||
const missing = this._missing
|
|
||||||
if (this.left !== null) {
|
|
||||||
missing.push(this.left)
|
|
||||||
}
|
|
||||||
if (this.right !== null) {
|
|
||||||
missing.push(this.right)
|
|
||||||
}
|
|
||||||
if (this.parent !== null) {
|
|
||||||
missing.push(this.parent)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @type {AbstractContent}
|
|
||||||
*/
|
|
||||||
this.content = readItemContent(decoder, info)
|
|
||||||
this.length = this.content.getLength()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Transaction} transaction
|
* @type {AbstractContent}
|
||||||
* @param {StructStore} store
|
|
||||||
* @param {number} offset
|
|
||||||
* @return {Item|GC}
|
|
||||||
*/
|
*/
|
||||||
toStruct (transaction, store, offset) {
|
const content = readItemContent(decoder, info)
|
||||||
if (offset > 0) {
|
|
||||||
this.id.clock += offset
|
|
||||||
this.left = createID(this.id.client, this.id.clock - 1)
|
|
||||||
this.content = this.content.splice(offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
const left = this.left === null ? null : getItemCleanEnd(transaction, store, this.left)
|
return new Item(id, null, origin, null, rightOrigin, parent, parentSub, content)
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const callTypeObservers = (type, transaction, event) => {
|
|||||||
if (type._item === null) {
|
if (type._item === null) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
type = type._item.parent
|
type = /** @type {AbstractType<any>} */ (type._item.parent)
|
||||||
}
|
}
|
||||||
callEventHandlerListeners(changedType._eH, event, transaction)
|
callEventHandlerListeners(changedType._eH, event, transaction)
|
||||||
}
|
}
|
||||||
@@ -386,7 +386,7 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
|
|||||||
const packJsonContent = () => {
|
const packJsonContent = () => {
|
||||||
if (jsonContent.length > 0) {
|
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 = 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 = []
|
jsonContent = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -405,12 +405,12 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
|
|||||||
case Uint8Array:
|
case Uint8Array:
|
||||||
case ArrayBuffer:
|
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 = 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
|
break
|
||||||
default:
|
default:
|
||||||
if (c instanceof AbstractType) {
|
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 = 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 {
|
} else {
|
||||||
throw new Error('Unexpected content type in insert operation')
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -45,13 +45,23 @@ export class YMapEvent extends YEvent {
|
|||||||
* @implements {Iterable<T>}
|
* @implements {Iterable<T>}
|
||||||
*/
|
*/
|
||||||
export class YMap extends AbstractType {
|
export class YMap extends AbstractType {
|
||||||
constructor () {
|
/**
|
||||||
|
*
|
||||||
|
* @param {Iterable<readonly [string, any]>=} entries - an optional iterable to initialize the YMap
|
||||||
|
*/
|
||||||
|
constructor (entries) {
|
||||||
super()
|
super()
|
||||||
/**
|
/**
|
||||||
* @type {Map<string,any>?}
|
* @type {Map<string,any>?}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this._prelimContent = new Map()
|
this._prelimContent = null
|
||||||
|
|
||||||
|
if (entries === undefined) {
|
||||||
|
this._prelimContent = new Map()
|
||||||
|
} else {
|
||||||
|
this._prelimContent = new Map(entries)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -105,6 +115,15 @@ export class YMap extends AbstractType {
|
|||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the size of the YMap (count of key/value pairs)
|
||||||
|
*
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
get size () {
|
||||||
|
return [...createMapIterator(this._map)].length
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the keys for each element in the YMap Type.
|
* Returns the keys for each element in the YMap Type.
|
||||||
*
|
*
|
||||||
@@ -133,7 +152,7 @@ export class YMap extends AbstractType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a provided function on once on overy key-value pair.
|
* Executes a provided function on once on every key-value pair.
|
||||||
*
|
*
|
||||||
* @param {function(T,string,YMap<T>):void} f A function to execute on every element of this YArray.
|
* @param {function(T,string,YMap<T>):void} f A function to execute on every element of this YArray.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -126,15 +126,14 @@ const findPosition = (transaction, parent, index) => {
|
|||||||
*
|
*
|
||||||
* @param {Transaction} transaction
|
* @param {Transaction} transaction
|
||||||
* @param {AbstractType<any>} parent
|
* @param {AbstractType<any>} parent
|
||||||
* @param {Item|null} left
|
* @param {ItemListPosition} currPos
|
||||||
* @param {Item|null} right
|
|
||||||
* @param {Map<string,any>} negatedAttributes
|
* @param {Map<string,any>} negatedAttributes
|
||||||
* @return {ItemListPosition}
|
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
const insertNegatedAttributes = (transaction, parent, left, right, negatedAttributes) => {
|
const insertNegatedAttributes = (transaction, parent, currPos, negatedAttributes) => {
|
||||||
|
let { left, right } = currPos
|
||||||
// check if we really need to remove attributes
|
// check if we really need to remove attributes
|
||||||
while (
|
while (
|
||||||
right !== null && (
|
right !== null && (
|
||||||
@@ -154,9 +153,10 @@ const insertNegatedAttributes = (transaction, parent, left, right, negatedAttrib
|
|||||||
const ownClientId = doc.clientID
|
const ownClientId = doc.clientID
|
||||||
for (const [key, val] of negatedAttributes) {
|
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 = 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)
|
||||||
}
|
}
|
||||||
return { left, right }
|
currPos.left = left
|
||||||
|
currPos.right = right
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -176,17 +176,16 @@ const updateCurrentAttributes = (currentAttributes, format) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Item|null} left
|
* @param {ItemListPosition} currPos
|
||||||
* @param {Item|null} right
|
|
||||||
* @param {Map<string,any>} currentAttributes
|
* @param {Map<string,any>} currentAttributes
|
||||||
* @param {Object<string,any>} attributes
|
* @param {Object<string,any>} attributes
|
||||||
* @return {ItemListPosition}
|
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
const minimizeAttributeChanges = (left, right, currentAttributes, attributes) => {
|
const minimizeAttributeChanges = (currPos, currentAttributes, attributes) => {
|
||||||
// go right while attributes[right.key] === right.value (or right is deleted)
|
// go right while attributes[right.key] === right.value (or right is deleted)
|
||||||
|
let { left, right } = currPos
|
||||||
while (true) {
|
while (true) {
|
||||||
if (right === null) {
|
if (right === null) {
|
||||||
break
|
break
|
||||||
@@ -201,22 +200,22 @@ const minimizeAttributeChanges = (left, right, currentAttributes, attributes) =>
|
|||||||
left = right
|
left = right
|
||||||
right = right.right
|
right = right.right
|
||||||
}
|
}
|
||||||
return new ItemListPosition(left, right)
|
currPos.left = left
|
||||||
|
currPos.right = right
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Transaction} transaction
|
* @param {Transaction} transaction
|
||||||
* @param {AbstractType<any>} parent
|
* @param {AbstractType<any>} parent
|
||||||
* @param {Item|null} left
|
* @param {ItemListPosition} currPos
|
||||||
* @param {Item|null} right
|
|
||||||
* @param {Map<string,any>} currentAttributes
|
* @param {Map<string,any>} currentAttributes
|
||||||
* @param {Object<string,any>} attributes
|
* @param {Object<string,any>} attributes
|
||||||
* @return {ItemInsertionResult}
|
* @return {Map<string,any>}
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @function
|
* @function
|
||||||
**/
|
**/
|
||||||
const insertAttributes = (transaction, parent, left, right, currentAttributes, attributes) => {
|
const insertAttributes = (transaction, parent, currPos, currentAttributes, attributes) => {
|
||||||
const doc = transaction.doc
|
const doc = transaction.doc
|
||||||
const ownClientId = doc.clientID
|
const ownClientId = doc.clientID
|
||||||
const negatedAttributes = new Map()
|
const negatedAttributes = new Map()
|
||||||
@@ -227,27 +226,26 @@ const insertAttributes = (transaction, parent, left, right, currentAttributes, a
|
|||||||
if (!equalAttrs(currentVal, val)) {
|
if (!equalAttrs(currentVal, val)) {
|
||||||
// save negated attribute (set null if currentVal undefined)
|
// save negated attribute (set null if currentVal undefined)
|
||||||
negatedAttributes.set(key, currentVal)
|
negatedAttributes.set(key, currentVal)
|
||||||
left = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
|
const { left, right } = currPos
|
||||||
left.integrate(transaction)
|
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, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new ItemInsertionResult(left, right, negatedAttributes)
|
return negatedAttributes
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Transaction} transaction
|
* @param {Transaction} transaction
|
||||||
* @param {AbstractType<any>} parent
|
* @param {AbstractType<any>} parent
|
||||||
* @param {Item|null} left
|
* @param {ItemListPosition} currPos
|
||||||
* @param {Item|null} right
|
|
||||||
* @param {Map<string,any>} currentAttributes
|
* @param {Map<string,any>} currentAttributes
|
||||||
* @param {string|object} text
|
* @param {string|object} text
|
||||||
* @param {Object<string,any>} attributes
|
* @param {Object<string,any>} attributes
|
||||||
* @return {ItemListPosition}
|
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @function
|
* @function
|
||||||
**/
|
**/
|
||||||
const insertText = (transaction, parent, left, right, currentAttributes, text, attributes) => {
|
const insertText = (transaction, parent, currPos, currentAttributes, text, attributes) => {
|
||||||
for (const [key] of currentAttributes) {
|
for (const [key] of currentAttributes) {
|
||||||
if (attributes[key] === undefined) {
|
if (attributes[key] === undefined) {
|
||||||
attributes[key] = null
|
attributes[key] = null
|
||||||
@@ -255,38 +253,33 @@ const insertText = (transaction, parent, left, right, currentAttributes, text, a
|
|||||||
}
|
}
|
||||||
const doc = transaction.doc
|
const doc = transaction.doc
|
||||||
const ownClientId = doc.clientID
|
const ownClientId = doc.clientID
|
||||||
const minPos = minimizeAttributeChanges(left, right, currentAttributes, attributes)
|
minimizeAttributeChanges(currPos, currentAttributes, attributes)
|
||||||
const insertPos = insertAttributes(transaction, parent, minPos.left, minPos.right, currentAttributes, attributes)
|
const negatedAttributes = insertAttributes(transaction, parent, currPos, currentAttributes, attributes)
|
||||||
left = insertPos.left
|
|
||||||
right = insertPos.right
|
|
||||||
// insert content
|
// insert content
|
||||||
const content = text.constructor === String ? new ContentString(/** @type {string} */ (text)) : new ContentEmbed(text)
|
const content = text.constructor === String ? new ContentString(/** @type {string} */ (text)) : new ContentEmbed(text)
|
||||||
left = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, content)
|
const { left, right } = currPos
|
||||||
left.integrate(transaction)
|
currPos.left = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, content)
|
||||||
return insertNegatedAttributes(transaction, parent, left, insertPos.right, insertPos.negatedAttributes)
|
currPos.left.integrate(transaction, 0)
|
||||||
|
return insertNegatedAttributes(transaction, parent, currPos, negatedAttributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Transaction} transaction
|
* @param {Transaction} transaction
|
||||||
* @param {AbstractType<any>} parent
|
* @param {AbstractType<any>} parent
|
||||||
* @param {Item|null} left
|
* @param {ItemListPosition} currPos
|
||||||
* @param {Item|null} right
|
|
||||||
* @param {Map<string,any>} currentAttributes
|
* @param {Map<string,any>} currentAttributes
|
||||||
* @param {number} length
|
* @param {number} length
|
||||||
* @param {Object<string,any>} attributes
|
* @param {Object<string,any>} attributes
|
||||||
* @return {ItemListPosition}
|
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
const formatText = (transaction, parent, left, right, currentAttributes, length, attributes) => {
|
const formatText = (transaction, parent, currPos, currentAttributes, length, attributes) => {
|
||||||
const doc = transaction.doc
|
const doc = transaction.doc
|
||||||
const ownClientId = doc.clientID
|
const ownClientId = doc.clientID
|
||||||
const minPos = minimizeAttributeChanges(left, right, currentAttributes, attributes)
|
minimizeAttributeChanges(currPos, currentAttributes, attributes)
|
||||||
const insertPos = insertAttributes(transaction, parent, minPos.left, minPos.right, currentAttributes, attributes)
|
const negatedAttributes = insertAttributes(transaction, parent, currPos, currentAttributes, attributes)
|
||||||
const negatedAttributes = insertPos.negatedAttributes
|
let { left, right } = currPos
|
||||||
left = insertPos.left
|
|
||||||
right = insertPos.right
|
|
||||||
// iterate until first non-format or null is found
|
// iterate until first non-format or null is found
|
||||||
// delete all formats with attributes[format.key] != null
|
// delete all formats with attributes[format.key] != null
|
||||||
while (length > 0 && right !== null) {
|
while (length > 0 && right !== null) {
|
||||||
@@ -327,9 +320,11 @@ const formatText = (transaction, parent, left, right, currentAttributes, length,
|
|||||||
newlines += '\n'
|
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 = 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)
|
||||||
}
|
}
|
||||||
return insertNegatedAttributes(transaction, parent, left, right, negatedAttributes)
|
currPos.left = left
|
||||||
|
currPos.right = right
|
||||||
|
insertNegatedAttributes(transaction, parent, currPos, negatedAttributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -438,8 +433,7 @@ export const cleanupYTextFormatting = type => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Transaction} transaction
|
* @param {Transaction} transaction
|
||||||
* @param {Item|null} left
|
* @param {ItemListPosition} currPos
|
||||||
* @param {Item|null} right
|
|
||||||
* @param {Map<string,any>} currentAttributes
|
* @param {Map<string,any>} currentAttributes
|
||||||
* @param {number} length
|
* @param {number} length
|
||||||
* @return {ItemListPosition}
|
* @return {ItemListPosition}
|
||||||
@@ -447,9 +441,10 @@ export const cleanupYTextFormatting = type => {
|
|||||||
* @private
|
* @private
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
const deleteText = (transaction, left, right, currentAttributes, length) => {
|
const deleteText = (transaction, currPos, currentAttributes, length) => {
|
||||||
const startAttrs = map.copy(currentAttributes)
|
const startAttrs = map.copy(currentAttributes)
|
||||||
const start = right
|
const start = currPos.right
|
||||||
|
let { left, right } = currPos
|
||||||
while (length > 0 && right !== null) {
|
while (length > 0 && right !== null) {
|
||||||
if (right.deleted === false) {
|
if (right.deleted === false) {
|
||||||
switch (right.content.constructor) {
|
switch (right.content.constructor) {
|
||||||
@@ -472,7 +467,9 @@ const deleteText = (transaction, left, right, currentAttributes, length) => {
|
|||||||
if (start) {
|
if (start) {
|
||||||
cleanupFormattingGap(transaction, start, right, startAttrs, map.copy(currentAttributes))
|
cleanupFormattingGap(transaction, start, right, startAttrs, map.copy(currentAttributes))
|
||||||
}
|
}
|
||||||
return { left, right }
|
currPos.left = left
|
||||||
|
currPos.right = right
|
||||||
|
return currPos
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -848,16 +845,19 @@ export class YText extends AbstractType {
|
|||||||
* Apply a {@link Delta} on this shared YText type.
|
* Apply a {@link Delta} on this shared YText type.
|
||||||
*
|
*
|
||||||
* @param {any} delta The changes to apply on this element.
|
* @param {any} delta The changes to apply on this element.
|
||||||
|
* @param {object} [opts]
|
||||||
|
* @param {boolean} [opts.sanitize] Sanitize input delta. Removes ending newlines if set to true.
|
||||||
|
*
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
applyDelta (delta) {
|
applyDelta (delta, { sanitize = true } = {}) {
|
||||||
if (this.doc !== null) {
|
if (this.doc !== null) {
|
||||||
transact(this.doc, transaction => {
|
transact(this.doc, transaction => {
|
||||||
/**
|
/**
|
||||||
* @type {ItemListPosition}
|
* @type {ItemListPosition}
|
||||||
*/
|
*/
|
||||||
let pos = new ItemListPosition(null, this._start)
|
const currPos = new ItemListPosition(null, this._start)
|
||||||
const currentAttributes = new Map()
|
const currentAttributes = new Map()
|
||||||
for (let i = 0; i < delta.length; i++) {
|
for (let i = 0; i < delta.length; i++) {
|
||||||
const op = delta[i]
|
const op = delta[i]
|
||||||
@@ -867,14 +867,14 @@ export class YText extends AbstractType {
|
|||||||
// there is a newline at the end of the content.
|
// there is a newline at the end of the content.
|
||||||
// If we omit this step, clients will see a different number of
|
// If we omit this step, clients will see a different number of
|
||||||
// paragraphs, but nothing bad will happen.
|
// paragraphs, but nothing bad will happen.
|
||||||
const ins = (typeof op.insert === 'string' && i === delta.length - 1 && pos.right === null && op.insert.slice(-1) === '\n') ? op.insert.slice(0, -1) : op.insert
|
const ins = (!sanitize && typeof op.insert === 'string' && i === delta.length - 1 && currPos.right === null && op.insert.slice(-1) === '\n') ? op.insert.slice(0, -1) : op.insert
|
||||||
if (typeof ins !== 'string' || ins.length > 0) {
|
if (typeof ins !== 'string' || ins.length > 0) {
|
||||||
pos = insertText(transaction, this, pos.left, pos.right, currentAttributes, ins, op.attributes || {})
|
insertText(transaction, this, currPos, currentAttributes, ins, op.attributes || {})
|
||||||
}
|
}
|
||||||
} else if (op.retain !== undefined) {
|
} else if (op.retain !== undefined) {
|
||||||
pos = formatText(transaction, this, pos.left, pos.right, currentAttributes, op.retain, op.attributes || {})
|
formatText(transaction, this, currPos, currentAttributes, op.retain, op.attributes || {})
|
||||||
} else if (op.delete !== undefined) {
|
} else if (op.delete !== undefined) {
|
||||||
pos = deleteText(transaction, pos.left, pos.right, currentAttributes, op.delete)
|
deleteText(transaction, currPos, currentAttributes, op.delete)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1012,7 +1012,7 @@ export class YText extends AbstractType {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
currentAttributes.forEach((v, k) => { attributes[k] = v })
|
currentAttributes.forEach((v, k) => { attributes[k] = v })
|
||||||
}
|
}
|
||||||
insertText(transaction, this, left, right, currentAttributes, text, attributes)
|
insertText(transaction, this, new ItemListPosition(left, right), currentAttributes, text, attributes)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
/** @type {Array<function>} */ (this._pending).push(() => this.insert(index, text, attributes))
|
/** @type {Array<function>} */ (this._pending).push(() => this.insert(index, text, attributes))
|
||||||
@@ -1037,7 +1037,7 @@ export class YText extends AbstractType {
|
|||||||
if (y !== null) {
|
if (y !== null) {
|
||||||
transact(y, transaction => {
|
transact(y, transaction => {
|
||||||
const { left, right, currentAttributes } = findPosition(transaction, this, index)
|
const { left, right, currentAttributes } = findPosition(transaction, this, index)
|
||||||
insertText(transaction, this, left, right, currentAttributes, embed, attributes)
|
insertText(transaction, this, new ItemListPosition(left, right), currentAttributes, embed, attributes)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
/** @type {Array<function>} */ (this._pending).push(() => this.insertEmbed(index, embed, attributes))
|
/** @type {Array<function>} */ (this._pending).push(() => this.insertEmbed(index, embed, attributes))
|
||||||
@@ -1060,7 +1060,7 @@ export class YText extends AbstractType {
|
|||||||
if (y !== null) {
|
if (y !== null) {
|
||||||
transact(y, transaction => {
|
transact(y, transaction => {
|
||||||
const { left, right, currentAttributes } = findPosition(transaction, this, index)
|
const { left, right, currentAttributes } = findPosition(transaction, this, index)
|
||||||
deleteText(transaction, left, right, currentAttributes, length)
|
deleteText(transaction, new ItemListPosition(left, right), currentAttributes, length)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
/** @type {Array<function>} */ (this._pending).push(() => this.delete(index, length))
|
/** @type {Array<function>} */ (this._pending).push(() => this.delete(index, length))
|
||||||
@@ -1088,7 +1088,7 @@ export class YText extends AbstractType {
|
|||||||
if (right === null) {
|
if (right === null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
formatText(transaction, this, left, right, currentAttributes, length, attributes)
|
formatText(transaction, this, new ItemListPosition(left, right), currentAttributes, length, attributes)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
/** @type {Array<function>} */ (this._pending).push(() => this.format(index, length, attributes))
|
/** @type {Array<function>} */ (this._pending).push(() => this.format(index, length, attributes))
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export class YXmlTreeWalker {
|
|||||||
} else if (n.parent === this._root) {
|
} else if (n.parent === this._root) {
|
||||||
n = null
|
n = null
|
||||||
} else {
|
} else {
|
||||||
n = n.parent._item
|
n = /** @type {AbstractType<any>} */ (n.parent)._item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
|
|||||||
if (!(right instanceof Item)) {
|
if (!(right instanceof Item)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
type = right.parent
|
type = /** @type {AbstractType<any>} */ (right.parent)
|
||||||
if (type._item === null || !type._item.deleted) {
|
if (type._item === null || !type._item.deleted) {
|
||||||
index = right.deleted || !right.countable ? 0 : res.diff
|
index = right.deleted || !right.countable ? 0 : res.diff
|
||||||
let n = right.left
|
let n = right.left
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import {
|
import {
|
||||||
GC,
|
GC,
|
||||||
splitItem,
|
splitItem,
|
||||||
AbstractStruct, GCRef, ItemRef, Transaction, ID, Item // eslint-disable-line
|
AbstractStruct, Transaction, ID, Item // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as math from 'lib0/math.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
|
* We could shift the array of refs instead, but shift is incredible
|
||||||
* slow in Chrome for arrays with more than 100k elements
|
* slow in Chrome for arrays with more than 100k elements
|
||||||
* @see tryResumePendingStructRefs
|
* @see tryResumePendingStructRefs
|
||||||
* @type {Map<number,{i:number,refs:Array<GCRef|ItemRef>}>}
|
* @type {Map<number,{i:number,refs:Array<GC|Item>}>}
|
||||||
*/
|
*/
|
||||||
this.pendingClientsStructRefs = new Map()
|
this.pendingClientsStructRefs = new Map()
|
||||||
/**
|
/**
|
||||||
* Stack of pending structs waiting for struct dependencies
|
* Stack of pending structs waiting for struct dependencies
|
||||||
* Maximum length of stack is structReaders.size
|
* Maximum length of stack is structReaders.size
|
||||||
* @type {Array<GCRef|ItemRef>}
|
* @type {Array<GC|Item>}
|
||||||
*/
|
*/
|
||||||
this.pendingStack = []
|
this.pendingStack = []
|
||||||
/**
|
/**
|
||||||
@@ -124,10 +124,18 @@ export const addStruct = (store, struct) => {
|
|||||||
export const findIndexSS = (structs, clock) => {
|
export const findIndexSS = (structs, clock) => {
|
||||||
let left = 0
|
let left = 0
|
||||||
let right = structs.length - 1
|
let right = structs.length - 1
|
||||||
|
let mid = structs[right]
|
||||||
|
let midclock = mid.id.clock
|
||||||
|
if (mid.id.clock === clock) {
|
||||||
|
return right
|
||||||
|
}
|
||||||
|
// @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) {
|
while (left <= right) {
|
||||||
const midindex = math.floor((left + right) / 2)
|
mid = structs[midindex]
|
||||||
const mid = structs[midindex]
|
midclock = mid.id.clock
|
||||||
const midclock = mid.id.clock
|
|
||||||
if (midclock <= clock) {
|
if (midclock <= clock) {
|
||||||
if (clock < midclock + mid.length) {
|
if (clock < midclock + mid.length) {
|
||||||
return midindex
|
return midindex
|
||||||
@@ -136,6 +144,7 @@ export const findIndexSS = (structs, clock) => {
|
|||||||
} else {
|
} else {
|
||||||
right = midindex - 1
|
right = midindex - 1
|
||||||
}
|
}
|
||||||
|
midindex = math.floor((left + right) / 2)
|
||||||
}
|
}
|
||||||
// Always check state before looking for a struct in StructStore
|
// Always check state before looking for a struct in StructStore
|
||||||
// Therefore the case of not finding a struct is unexpected
|
// Therefore the case of not finding a struct is unexpected
|
||||||
|
|||||||
@@ -156,8 +156,8 @@ const tryToMergeWithLeft = (structs, pos) => {
|
|||||||
if (left.deleted === right.deleted && left.constructor === right.constructor) {
|
if (left.deleted === right.deleted && left.constructor === right.constructor) {
|
||||||
if (left.mergeWith(right)) {
|
if (left.mergeWith(right)) {
|
||||||
structs.splice(pos, 1)
|
structs.splice(pos, 1)
|
||||||
if (right instanceof Item && right.parentSub !== null && right.parent._map.get(right.parentSub) === right) {
|
if (right instanceof Item && right.parentSub !== null && /** @type {AbstractType<any>} */ (right.parent)._map.get(right.parentSub) === right) {
|
||||||
right.parent._map.set(right.parentSub, /** @type {Item} */ (left))
|
/** @type {AbstractType<any>} */ (right.parent)._map.set(right.parentSub, /** @type {Item} */ (left))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ const getPathTo = (parent, child) => {
|
|||||||
} else {
|
} else {
|
||||||
// parent is array-ish
|
// parent is array-ish
|
||||||
let i = 0
|
let i = 0
|
||||||
let c = child._item.parent._start
|
let c = /** @type {AbstractType<any>} */ (child._item.parent)._start
|
||||||
while (c !== child._item && c !== null) {
|
while (c !== child._item && c !== null) {
|
||||||
if (!c.deleted) {
|
if (!c.deleted) {
|
||||||
i++
|
i++
|
||||||
@@ -220,7 +220,7 @@ const getPathTo = (parent, child) => {
|
|||||||
}
|
}
|
||||||
path.unshift(i)
|
path.unshift(i)
|
||||||
}
|
}
|
||||||
child = child._item.parent
|
child = /** @type {AbstractType<any>} */ (child._item.parent)
|
||||||
}
|
}
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,6 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
findIndexSS,
|
findIndexSS,
|
||||||
GCRef,
|
|
||||||
ItemRef,
|
|
||||||
writeID,
|
writeID,
|
||||||
readID,
|
readID,
|
||||||
getState,
|
getState,
|
||||||
@@ -27,6 +25,7 @@ import {
|
|||||||
writeDeleteSet,
|
writeDeleteSet,
|
||||||
createDeleteSetFromStructStore,
|
createDeleteSetFromStructStore,
|
||||||
transact,
|
transact,
|
||||||
|
readItem,
|
||||||
Doc, Transaction, GC, Item, StructStore, ID // eslint-disable-line
|
Doc, Transaction, GC, Item, StructStore, ID // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
@@ -80,7 +79,9 @@ export const writeClientsStructs = (encoder, store, _sm) => {
|
|||||||
})
|
})
|
||||||
// write # states that were updated
|
// write # states that were updated
|
||||||
encoding.writeVarUint(encoder, sm.size)
|
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
|
// @ts-ignore
|
||||||
writeStructs(encoder, store.clients.get(client), client, clock)
|
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 {decoding.Decoder} decoder The decoder object to read data from.
|
||||||
* @param {Map<number,Array<GCRef|ItemRef>>} clientRefs
|
* @param {Map<number,Array<GC|Item>>} clientRefs
|
||||||
* @return {Map<number,Array<GCRef|ItemRef>>}
|
* @param {Doc} doc
|
||||||
|
* @return {Map<number,Array<GC|Item>>}
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const readClientsStructRefs = (decoder, clientRefs) => {
|
export const readClientsStructRefs = (decoder, clientRefs, doc) => {
|
||||||
const numOfStateUpdates = decoding.readVarUint(decoder)
|
const numOfStateUpdates = decoding.readVarUint(decoder)
|
||||||
for (let i = 0; i < numOfStateUpdates; i++) {
|
for (let i = 0; i < numOfStateUpdates; i++) {
|
||||||
const numberOfStructs = decoding.readVarUint(decoder)
|
const numberOfStructs = decoding.readVarUint(decoder)
|
||||||
@@ -102,15 +104,16 @@ export const readClientsStructRefs = (decoder, clientRefs) => {
|
|||||||
const nextIdClient = nextID.client
|
const nextIdClient = nextID.client
|
||||||
let nextIdClock = nextID.clock
|
let nextIdClock = nextID.clock
|
||||||
/**
|
/**
|
||||||
* @type {Array<GCRef|ItemRef>}
|
* @type {Array<GC|Item>}
|
||||||
*/
|
*/
|
||||||
const refs = []
|
const refs = []
|
||||||
clientRefs.set(nextIdClient, refs)
|
clientRefs.set(nextIdClient, refs)
|
||||||
for (let i = 0; i < numberOfStructs; i++) {
|
for (let i = 0; i < numberOfStructs; i++) {
|
||||||
const info = decoding.readUint8(decoder)
|
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)
|
const id = createID(nextIdClient, nextIdClock)
|
||||||
refs.push(ref)
|
const struct = (binary.BITS5 & info) === 0 ? new GC(id, decoding.readVarUint(decoder)) : readItem(decoder, id, info, doc)
|
||||||
nextIdClock += ref.length
|
refs.push(struct)
|
||||||
|
nextIdClock += struct.length
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return clientRefs
|
return clientRefs
|
||||||
@@ -155,12 +158,12 @@ const resumeStructIntegration = (transaction, store) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const ref = stack[stack.length - 1]
|
const ref = stack[stack.length - 1]
|
||||||
const m = ref._missing
|
|
||||||
const refID = ref.id
|
const refID = ref.id
|
||||||
const client = refID.client
|
const client = refID.client
|
||||||
const refClock = refID.clock
|
const refClock = refID.clock
|
||||||
const localClock = getState(store, client)
|
const localClock = getState(store, client)
|
||||||
const offset = refClock < localClock ? localClock - refClock : 0
|
const offset = refClock < localClock ? localClock - refClock : 0
|
||||||
|
const missing = ref.getMissing(transaction, store)
|
||||||
if (refClock + offset !== localClock) {
|
if (refClock + offset !== localClock) {
|
||||||
// A previous message from this client is missing
|
// A previous message from this client is missing
|
||||||
// check if there is a pending structRef with a smaller clock and switch them
|
// check if there is a pending structRef with a smaller clock and switch them
|
||||||
@@ -180,27 +183,21 @@ const resumeStructIntegration = (transaction, store) => {
|
|||||||
// wait until missing struct is available
|
// wait until missing struct is available
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
while (m.length > 0) {
|
if (missing) {
|
||||||
const missing = m[m.length - 1]
|
const client = missing.client
|
||||||
if (getState(store, missing.client) <= missing.clock) {
|
// get the struct reader that has the missing struct
|
||||||
const client = missing.client
|
const structRefs = clientsStructRefs.get(client)
|
||||||
// get the struct reader that has the missing struct
|
if (structRefs === undefined) {
|
||||||
const structRefs = clientsStructRefs.get(client)
|
// This update message causally depends on another update message.
|
||||||
if (structRefs === undefined) {
|
return
|
||||||
// This update message causally depends on another update message.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
stack.push(structRefs.refs[structRefs.i++])
|
|
||||||
if (structRefs.i === structRefs.refs.length) {
|
|
||||||
clientsStructRefs.delete(client)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
ref._missing.pop()
|
stack.push(structRefs.refs[structRefs.i++])
|
||||||
}
|
if (structRefs.i === structRefs.refs.length) {
|
||||||
if (m.length === 0) {
|
clientsStructRefs.delete(client)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (offset < ref.length) {
|
if (offset < ref.length) {
|
||||||
ref.toStruct(transaction, store, offset).integrate(transaction)
|
ref.integrate(transaction, offset)
|
||||||
}
|
}
|
||||||
stack.pop()
|
stack.pop()
|
||||||
}
|
}
|
||||||
@@ -233,7 +230,7 @@ export const writeStructsFromTransaction = (encoder, transaction) => writeClient
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {StructStore} store
|
* @param {StructStore} store
|
||||||
* @param {Map<number, Array<GCRef|ItemRef>>} clientsStructsRefs
|
* @param {Map<number, Array<GC|Item>>} clientsStructsRefs
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @function
|
* @function
|
||||||
@@ -270,7 +267,7 @@ const mergeReadStructsIntoPendingReads = (store, clientsStructsRefs) => {
|
|||||||
*/
|
*/
|
||||||
export const readStructs = (decoder, transaction, store) => {
|
export const readStructs = (decoder, transaction, store) => {
|
||||||
const clientsStructRefs = new Map()
|
const clientsStructRefs = new Map()
|
||||||
readClientsStructRefs(decoder, clientsStructRefs)
|
readClientsStructRefs(decoder, clientsStructRefs, transaction.doc)
|
||||||
mergeReadStructsIntoPendingReads(store, clientsStructRefs)
|
mergeReadStructsIntoPendingReads(store, clientsStructRefs)
|
||||||
resumeStructIntegration(transaction, store)
|
resumeStructIntegration(transaction, store)
|
||||||
tryResumePendingDeleteReaders(transaction, store)
|
tryResumePendingDeleteReaders(transaction, store)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const isParentOf = (parent, child) => {
|
|||||||
if (child.parent === parent) {
|
if (child.parent === parent) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
child = child.parent._item
|
child = /** @type {AbstractType<any>} */ (child.parent)._item
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,33 @@ import * as Y from '../src/index.js'
|
|||||||
import * as t from 'lib0/testing.js'
|
import * as t from 'lib0/testing.js'
|
||||||
import * as prng from 'lib0/prng.js'
|
import * as prng from 'lib0/prng.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testMapHavingIterableAsConstructorParamTests = tc => {
|
||||||
|
const { map0 } = init(tc, { users: 1 })
|
||||||
|
|
||||||
|
const m1 = new Y.Map(Object.entries({ number: 1, string: 'hello' }))
|
||||||
|
map0.set('m1', m1)
|
||||||
|
t.assert(m1.get('number') === 1)
|
||||||
|
t.assert(m1.get('string') === 'hello')
|
||||||
|
|
||||||
|
const m2 = new Y.Map([
|
||||||
|
['object', { x: 1 }],
|
||||||
|
['boolean', true]
|
||||||
|
])
|
||||||
|
map0.set('m2', m2)
|
||||||
|
t.assert(m2.get('object').x === 1)
|
||||||
|
t.assert(m2.get('boolean') === true)
|
||||||
|
|
||||||
|
const m3 = new Y.Map([...m1, ...m2])
|
||||||
|
map0.set('m3', m3)
|
||||||
|
t.assert(m3.get('number') === 1)
|
||||||
|
t.assert(m3.get('string') === 'hello')
|
||||||
|
t.assert(m3.get('object').x === 1)
|
||||||
|
t.assert(m3.get('boolean') === true)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
@@ -33,6 +60,7 @@ export const testBasicMapTests = tc => {
|
|||||||
t.assert(map0.get('boolean1') === true, 'client 0 computed the change (boolean)')
|
t.assert(map0.get('boolean1') === true, 'client 0 computed the change (boolean)')
|
||||||
t.compare(map0.get('object'), { key: { key2: 'value' } }, 'client 0 computed the change (object)')
|
t.compare(map0.get('object'), { key: { key2: 'value' } }, 'client 0 computed the change (object)')
|
||||||
t.assert(map0.get('y-map').get('y-array').get(0) === -1, 'client 0 computed the change (type)')
|
t.assert(map0.get('y-map').get('y-array').get(0) === -1, 'client 0 computed the change (type)')
|
||||||
|
t.assert(map0.size === 6, 'client 0 map has correct size')
|
||||||
|
|
||||||
users[2].connect()
|
users[2].connect()
|
||||||
testConnector.flushAllMessages()
|
testConnector.flushAllMessages()
|
||||||
@@ -43,6 +71,7 @@ export const testBasicMapTests = tc => {
|
|||||||
t.assert(map1.get('boolean1') === true, 'client 1 computed the change (boolean)')
|
t.assert(map1.get('boolean1') === true, 'client 1 computed the change (boolean)')
|
||||||
t.compare(map1.get('object'), { key: { key2: 'value' } }, 'client 1 received the update (object)')
|
t.compare(map1.get('object'), { key: { key2: 'value' } }, 'client 1 received the update (object)')
|
||||||
t.assert(map1.get('y-map').get('y-array').get(0) === -1, 'client 1 received the update (type)')
|
t.assert(map1.get('y-map').get('y-array').get(0) === -1, 'client 1 received the update (type)')
|
||||||
|
t.assert(map1.size === 6, 'client 1 map has correct size')
|
||||||
|
|
||||||
// compare disconnected user
|
// compare disconnected user
|
||||||
t.assert(map2.get('number') === 1, 'client 2 received the update (number) - was disconnected')
|
t.assert(map2.get('number') === 1, 'client 2 received the update (number) - was disconnected')
|
||||||
@@ -130,6 +159,20 @@ export const testGetAndSetOfMapPropertyWithConflict = tc => {
|
|||||||
compare(users)
|
compare(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testSizeAndDeleteOfMapProperty = tc => {
|
||||||
|
const { map0 } = init(tc, { users: 1 })
|
||||||
|
map0.set('stuff', 'c0')
|
||||||
|
map0.set('otherstuff', 'c1')
|
||||||
|
t.assert(map0.size === 2, `map size is ${map0.size} expected 2`)
|
||||||
|
map0.delete('stuff')
|
||||||
|
t.assert(map0.size === 1, `map size after delete is ${map0.size}, expected 1`)
|
||||||
|
map0.delete('otherstuff')
|
||||||
|
t.assert(map0.size === 0, `map size after delete is ${map0.size}, expected 0`)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
@@ -454,7 +497,7 @@ const mapTransactions = [
|
|||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testRepeatGeneratingYmapTests10 = tc => {
|
export const testRepeatGeneratingYmapTests10 = tc => {
|
||||||
applyRandomTests(tc, mapTransactions, 10)
|
applyRandomTests(tc, mapTransactions, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -207,20 +207,31 @@ export const testFormattingRemovedInMidText = tc => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*
|
*/
|
||||||
export const testLargeFragmentedDocument = tc => {
|
export const testLargeFragmentedDocument = tc => {
|
||||||
const { text0, text1, testConnector } = init(tc, { users: 2 })
|
const itemsToInsert = 1000000
|
||||||
// @ts-ignore
|
let update = /** @type {any} */ (null)
|
||||||
text0.doc.transact(() => {
|
;(() => {
|
||||||
for (let i = 0; i < 1000000; i++) {
|
const doc1 = new Y.Doc()
|
||||||
text0.insert(0, '0')
|
const text0 = doc1.getText('txt')
|
||||||
}
|
t.measureTime(`time to insert ${itemsToInsert}`, () => {
|
||||||
})
|
doc1.transact(() => {
|
||||||
t.measureTime('time to apply', () => {
|
for (let i = 0; i < itemsToInsert; i++) {
|
||||||
testConnector.flushAllMessages()
|
text0.insert(0, '0')
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
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
|
// RANDOM TESTS
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user