mirror of
https://github.com/yjs/yjs.git
synced 2025-12-16 11:47:46 +01:00
use "insertSet" instead of computing state vectors in transactions
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
readYXmlFragment,
|
||||
readYXmlHook,
|
||||
readYXmlText,
|
||||
isDeleted,
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item, YEvent, AbstractType // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
@@ -107,7 +108,7 @@ export class ContentType {
|
||||
while (item !== null) {
|
||||
if (!item.deleted) {
|
||||
item.delete(transaction)
|
||||
} else if (item.id.clock < (transaction.beforeState.get(item.id.client) || 0)) {
|
||||
} else if (!isDeleted(transaction.insertSet, item.id)) {
|
||||
// This will be gc'd later and we want to merge it if possible
|
||||
// We try to merge all deleted items after each transaction,
|
||||
// but we have no knowledge about that this needs to be merged
|
||||
@@ -119,7 +120,7 @@ export class ContentType {
|
||||
this.type._map.forEach(item => {
|
||||
if (!item.deleted) {
|
||||
item.delete(transaction)
|
||||
} else if (item.id.clock < (transaction.beforeState.get(item.id.client) || 0)) {
|
||||
} else if (!isDeleted(transaction.insertSet, item.id)) {
|
||||
// same as above
|
||||
transaction._mergeStructs.push(item)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import {
|
||||
AbstractStruct,
|
||||
addStruct,
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, ID // eslint-disable-line
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, ID, // eslint-disable-line
|
||||
addItemToInsertSet
|
||||
} from '../internals.js'
|
||||
|
||||
export const structGCRefNumber = 0
|
||||
@@ -37,6 +38,7 @@ export class GC extends AbstractStruct {
|
||||
this.id.clock += offset
|
||||
this.length -= offset
|
||||
}
|
||||
addItemToInsertSet(transaction, this)
|
||||
addStruct(transaction.doc.store, this)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ import {
|
||||
readContentType,
|
||||
addChangedTypeToTransaction,
|
||||
isDeleted,
|
||||
StackItem, DeleteSet, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line
|
||||
StackItem, DeleteSet, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction, // eslint-disable-line
|
||||
addItemToInsertSet
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error'
|
||||
@@ -514,6 +515,7 @@ export class Item extends AbstractStruct {
|
||||
if (this.parentSub === null && this.countable && !this.deleted) {
|
||||
/** @type {AbstractType<any>} */ (this.parent)._length += this.length
|
||||
}
|
||||
addItemToInsertSet(transaction, this)
|
||||
addStruct(transaction.doc.store, this)
|
||||
this.content.integrate(transaction, this)
|
||||
// add parent to transaction.changed
|
||||
|
||||
@@ -493,19 +493,13 @@ export const cleanupYTextAfterTransaction = transaction => {
|
||||
const needFullCleanup = new Set()
|
||||
// check if another formatting item was inserted
|
||||
const doc = transaction.doc
|
||||
for (const [client, afterClock] of transaction.afterState.entries()) {
|
||||
const clock = transaction.beforeState.get(client) || 0
|
||||
if (afterClock === clock) {
|
||||
continue
|
||||
iterateDeletedStructs(transaction, transaction.insertSet, (item) => {
|
||||
if (
|
||||
!item.deleted && /** @type {Item} */ (item).content.constructor === ContentFormat && item.constructor !== GC
|
||||
) {
|
||||
needFullCleanup.add(/** @type {any} */ (item).parent)
|
||||
}
|
||||
iterateStructs(transaction, /** @type {Array<Item|GC>} */ (doc.store.clients.get(client)), clock, afterClock, item => {
|
||||
if (
|
||||
!item.deleted && /** @type {Item} */ (item).content.constructor === ContentFormat && item.constructor !== GC
|
||||
) {
|
||||
needFullCleanup.add(/** @type {any} */ (item).parent)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
// cleanup in a new transaction
|
||||
transact(doc, (t) => {
|
||||
iterateDeletedStructs(transaction, transaction.deleteSet, item => {
|
||||
|
||||
@@ -11,9 +11,12 @@ import {
|
||||
generateNewClientId,
|
||||
createID,
|
||||
cleanupYTextAfterTransaction,
|
||||
UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
|
||||
isDeleted,
|
||||
UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractType, AbstractStruct, YEvent, Doc, // eslint-disable-line
|
||||
DeleteItem
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error'
|
||||
import * as map from 'lib0/map'
|
||||
import * as math from 'lib0/math'
|
||||
import * as set from 'lib0/set'
|
||||
@@ -63,15 +66,20 @@ export class Transaction {
|
||||
*/
|
||||
this.deleteSet = new DeleteSet()
|
||||
/**
|
||||
* Holds the state before the transaction started.
|
||||
* @type {Map<Number,Number>}
|
||||
* Describes the set of inserted items by ids
|
||||
* @type {DeleteSet}
|
||||
*/
|
||||
this.beforeState = getStateVector(doc.store)
|
||||
this.insertSet = new DeleteSet()
|
||||
/**
|
||||
* Holds the state before the transaction started.
|
||||
* @type {Map<Number,Number>?}
|
||||
*/
|
||||
this._beforeState = null
|
||||
/**
|
||||
* Holds the state after the transaction.
|
||||
* @type {Map<Number,Number>}
|
||||
* @type {Map<Number,Number>?}
|
||||
*/
|
||||
this.afterState = new Map()
|
||||
this._afterState = null
|
||||
/**
|
||||
* All types that were directly modified (property added or child
|
||||
* inserted/deleted). New types are not included in this Set.
|
||||
@@ -119,6 +127,43 @@ export class Transaction {
|
||||
* @type {boolean}
|
||||
*/
|
||||
this._needFormattingCleanup = false
|
||||
this._done = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the state before the transaction started.
|
||||
*
|
||||
* @deprecated
|
||||
* @type {Map<Number,Number>}
|
||||
*/
|
||||
get beforeState () {
|
||||
if (this._beforeState == null) {
|
||||
const sv = getStateVector(this.doc.store)
|
||||
this.insertSet.clients.forEach((ranges, client) => {
|
||||
sv.set(client, ranges[0].clock)
|
||||
})
|
||||
this._beforeState = sv
|
||||
}
|
||||
return this._beforeState
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the state after the transaction.
|
||||
*
|
||||
* @deprecated
|
||||
* @type {Map<Number,Number>}
|
||||
*/
|
||||
get afterState () {
|
||||
if (!this._done) error.unexpectedCase()
|
||||
if (this._afterState == null) {
|
||||
const sv = getStateVector(this.doc.store)
|
||||
this.insertSet.clients.forEach((ranges, client) => {
|
||||
const d = ranges[ranges.length - 1]
|
||||
sv.set(client, d.clock + d.len)
|
||||
})
|
||||
this._afterState = sv
|
||||
}
|
||||
return this._afterState
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +173,7 @@ export class Transaction {
|
||||
* @return {boolean} Whether data was written.
|
||||
*/
|
||||
export const writeUpdateMessageFromTransaction = (encoder, transaction) => {
|
||||
if (transaction.deleteSet.clients.size === 0 && !map.any(transaction.afterState, (clock, client) => transaction.beforeState.get(client) !== clock)) {
|
||||
if (transaction.deleteSet.clients.size === 0 && transaction.insertSet.clients.size === 0) {
|
||||
return false
|
||||
}
|
||||
sortAndMergeDeleteSet(transaction.deleteSet)
|
||||
@@ -158,11 +203,28 @@ export const nextID = transaction => {
|
||||
*/
|
||||
export const addChangedTypeToTransaction = (transaction, type, parentSub) => {
|
||||
const item = type._item
|
||||
if (item === null || (item.id.clock < (transaction.beforeState.get(item.id.client) || 0) && !item.deleted)) {
|
||||
if (item === null || (!item.deleted && !isDeleted(transaction.insertSet, item.id))) {
|
||||
map.setIfUndefined(transaction.changed, type, set.create).add(parentSub)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} tr
|
||||
* @param {AbstractStruct} item
|
||||
*/
|
||||
export const addItemToInsertSet = (tr, item) => {
|
||||
const ranges = map.setIfUndefined(tr.insertSet.clients, item.id.client, () => /** @type {Array<import('./DeleteSet.js').DeleteItem>} */ ([]))
|
||||
if (ranges.length > 0) {
|
||||
const r = ranges[ranges.length - 1]
|
||||
if (r.clock + r.len === item.id.clock) {
|
||||
// @ts-ignore
|
||||
r.len += item.length
|
||||
return
|
||||
}
|
||||
}
|
||||
ranges.push(new DeleteItem(item.id.clock, item.length))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<AbstractStruct>} structs
|
||||
* @param {number} pos
|
||||
@@ -260,13 +322,15 @@ export const tryGc = (ds, store, gcFilter) => {
|
||||
const cleanupTransactions = (transactionCleanups, i) => {
|
||||
if (i < transactionCleanups.length) {
|
||||
const transaction = transactionCleanups[i]
|
||||
transaction._done = true
|
||||
const doc = transaction.doc
|
||||
const store = doc.store
|
||||
const ds = transaction.deleteSet
|
||||
const insertSet = transaction.insertSet
|
||||
const mergeStructs = transaction._mergeStructs
|
||||
try {
|
||||
sortAndMergeDeleteSet(ds)
|
||||
transaction.afterState = getStateVector(transaction.doc.store)
|
||||
sortAndMergeDeleteSet(insertSet)
|
||||
doc.emit('beforeObserverCalls', [transaction, doc])
|
||||
/**
|
||||
* An array of event callbacks.
|
||||
@@ -323,15 +387,13 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
||||
tryMergeDeleteSet(ds, store)
|
||||
|
||||
// on all affected store.clients props, try to merge
|
||||
transaction.afterState.forEach((clock, client) => {
|
||||
const beforeClock = transaction.beforeState.get(client) || 0
|
||||
if (beforeClock !== clock) {
|
||||
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
|
||||
// we iterate from right to left so we can safely remove entries
|
||||
const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1)
|
||||
for (let i = structs.length - 1; i >= firstChangePos;) {
|
||||
i -= 1 + tryToMergeWithLefts(structs, i)
|
||||
}
|
||||
transaction.insertSet.clients.forEach((ids, client) => {
|
||||
const firstClock = ids[0].clock
|
||||
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
|
||||
// we iterate from right to left so we can safely remove entries
|
||||
const firstChangePos = math.max(findIndexSS(structs, firstClock), 1)
|
||||
for (let i = structs.length - 1; i >= firstChangePos;) {
|
||||
i -= 1 + tryToMergeWithLefts(structs, i)
|
||||
}
|
||||
})
|
||||
// try to merge mergeStructs
|
||||
@@ -350,7 +412,7 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
||||
tryToMergeWithLefts(structs, replacedStructPos)
|
||||
}
|
||||
}
|
||||
if (!transaction.local && transaction.afterState.get(doc.clientID) !== transaction.beforeState.get(doc.clientID)) {
|
||||
if (!transaction.local && transaction.insertSet.clients.has(doc.clientID)) {
|
||||
logging.print(logging.ORANGE, logging.BOLD, '[yjs] ', logging.UNBOLD, logging.RED, 'Changed the client-id because another client seems to be using it.')
|
||||
doc.clientID = generateNewClientId()
|
||||
}
|
||||
|
||||
@@ -226,14 +226,7 @@ export class UndoManager extends ObservableV2 {
|
||||
// neither undoing nor redoing: delete redoStack
|
||||
this.clear(false, true)
|
||||
}
|
||||
const insertions = new DeleteSet()
|
||||
transaction.afterState.forEach((endClock, client) => {
|
||||
const startClock = transaction.beforeState.get(client) || 0
|
||||
const len = endClock - startClock
|
||||
if (len > 0) {
|
||||
addToDeleteSet(insertions, client, startClock, len)
|
||||
}
|
||||
})
|
||||
const insertions = transaction.insertSet
|
||||
const now = time.getUnixTime()
|
||||
let didAdd = false
|
||||
if (this.lastChange > 0 && now - this.lastChange < this.captureTimeout && stack.length > 0 && !undoing && !redoing) {
|
||||
|
||||
@@ -158,7 +158,7 @@ export class YEvent {
|
||||
* @return {boolean}
|
||||
*/
|
||||
adds (struct) {
|
||||
return struct.id.clock >= (this.transaction.beforeState.get(struct.id.client) || 0)
|
||||
return isDeleted(this.transaction.insertSet, struct.id)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,7 +36,8 @@ import {
|
||||
Skip,
|
||||
diffUpdateV2,
|
||||
convertUpdateFormatV2ToV1,
|
||||
DSDecoderV2, Doc, Transaction, GC, Item, StructStore // eslint-disable-line
|
||||
DeleteSet, DSDecoderV2, Doc, Transaction, GC, Item, StructStore, // eslint-disable-line
|
||||
iterateDeletedStructs
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding'
|
||||
@@ -101,6 +102,26 @@ export const writeClientsStructs = (encoder, store, _sm) => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {StructStore} store
|
||||
* @param {DeleteSet} idset
|
||||
*
|
||||
* @todo at the moment this writes the full deleteset range
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const writeStructsFromIdSet = (encoder, store, idset) => {
|
||||
// write # states that were updated
|
||||
encoding.writeVarUint(encoder.restEncoder, idset.clients.size)
|
||||
// Write items with higher client ids first
|
||||
// This heavily improves the conflict algorithm.
|
||||
array.from(idset.clients.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, ids]) => {
|
||||
writeStructs(encoder, /** @type {Array<GC|Item>} */ (store.clients.get(client)), client, ids[0].clock)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder The decoder object to read data from.
|
||||
* @param {Doc} doc
|
||||
@@ -365,7 +386,7 @@ const integrateStructs = (transaction, store, clientsStructRefs) => {
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const writeStructsFromTransaction = (encoder, transaction) => writeClientsStructs(encoder, transaction.doc.store, transaction.beforeState)
|
||||
export const writeStructsFromTransaction = (encoder, transaction) => writeStructsFromIdSet(encoder, transaction.doc.store, transaction.insertSet)
|
||||
|
||||
/**
|
||||
* Read and apply a document update.
|
||||
|
||||
Reference in New Issue
Block a user