implement some of the commented todos
This commit is contained in:
@@ -193,7 +193,7 @@ export const readDeleteSet = (decoder, transaction, store) => {
|
||||
let struct = structs[index]
|
||||
// split the first item if necessary
|
||||
if (!struct.deleted && struct.id.clock < clock) {
|
||||
structs.splice(index + 1, 0, struct.splitAt(store, clock - struct.id.clock))
|
||||
structs.splice(index + 1, 0, struct.splitAt(clock - struct.id.clock))
|
||||
index++ // increase we now want to use the next struct
|
||||
}
|
||||
while (index < structs.length) {
|
||||
@@ -202,7 +202,7 @@ export const readDeleteSet = (decoder, transaction, store) => {
|
||||
if (struct.id.clock < clock + len) {
|
||||
if (!struct.deleted) {
|
||||
if (clock + len < struct.id.clock + struct.length) {
|
||||
structs.splice(index, 0, struct.splitAt(store, clock + len - struct.id.clock))
|
||||
structs.splice(index, 0, struct.splitAt(clock + len - struct.id.clock))
|
||||
}
|
||||
struct.delete(transaction)
|
||||
}
|
||||
|
||||
@@ -24,27 +24,13 @@ export class ID {
|
||||
this.clock = clock
|
||||
}
|
||||
/**
|
||||
* @return {ID}
|
||||
* @deprecated
|
||||
* @todo remove and adapt relative position implementation
|
||||
*/
|
||||
clone () {
|
||||
return new ID(this.client, this.clock)
|
||||
}
|
||||
/**
|
||||
* @param {ID} id
|
||||
* @return {boolean}
|
||||
*/
|
||||
equals (id) {
|
||||
return id !== null && id.client === this.client && id.clock === this.clock
|
||||
}
|
||||
/**
|
||||
* @param {ID} id
|
||||
* @return {boolean}
|
||||
*/
|
||||
lessThan (id) {
|
||||
if (id.constructor === ID) {
|
||||
return this.client < id.client || (this.client === id.client && this.clock < id.clock)
|
||||
} else {
|
||||
return false
|
||||
toJSON () {
|
||||
return {
|
||||
client: this.client,
|
||||
clock: this.clock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ export const getItemCleanStart = (store, id) => {
|
||||
*/
|
||||
let struct = structs[index]
|
||||
if (struct.id.clock < id.clock && struct.constructor !== GC) {
|
||||
struct = struct.splitAt(store, id.clock - struct.id.clock)
|
||||
struct = struct.splitAt(id.clock - struct.id.clock)
|
||||
structs.splice(index + 1, 0, struct)
|
||||
}
|
||||
return struct
|
||||
@@ -198,7 +198,7 @@ export const getItemCleanEnd = (store, id) => {
|
||||
const index = findIndexSS(structs, id.clock)
|
||||
const struct = structs[index]
|
||||
if (id.clock !== struct.id.clock + struct.length - 1 && struct.constructor !== GC) {
|
||||
structs.splice(index + 1, 0, struct.splitAt(store, id.clock - struct.id.clock + 1))
|
||||
structs.splice(index + 1, 0, struct.splitAt(id.clock - struct.id.clock + 1))
|
||||
}
|
||||
return struct
|
||||
}
|
||||
|
||||
@@ -10,11 +10,16 @@ import {
|
||||
DeleteSet,
|
||||
sortAndMergeDeleteSet,
|
||||
getStates,
|
||||
findIndexSS,
|
||||
callEventHandlerListeners,
|
||||
AbstractItem,
|
||||
ItemDeleted,
|
||||
AbstractType, AbstractStruct, YEvent, Y // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as map from 'lib0/map.js'
|
||||
import * as math from 'lib0/math.js'
|
||||
|
||||
/**
|
||||
* A transaction is created for every change on the Yjs model. It is possible
|
||||
@@ -108,3 +113,119 @@ export const nextID = transaction => {
|
||||
const y = transaction.y
|
||||
return createID(y.clientID, getState(y.store, y.clientID))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Y} y
|
||||
* @param {function(Transaction):void} f
|
||||
*/
|
||||
export const transact = (y, f) => {
|
||||
let initialCall = false
|
||||
if (y._transaction === null) {
|
||||
initialCall = true
|
||||
y._transaction = new Transaction(y)
|
||||
y.emit('beforeTransaction', [y, y._transaction])
|
||||
}
|
||||
const transaction = y._transaction
|
||||
try {
|
||||
f(transaction)
|
||||
} finally {
|
||||
if (initialCall) {
|
||||
y._transaction = null
|
||||
y.emit('beforeObserverCalls', [y, y._transaction])
|
||||
// emit change events on changed types
|
||||
transaction.changed.forEach((subs, itemtype) => {
|
||||
itemtype._callObserver(transaction, subs)
|
||||
})
|
||||
transaction.changedParentTypes.forEach((events, type) => {
|
||||
events = events
|
||||
.filter(event =>
|
||||
event.target._item === null || !event.target._item.deleted
|
||||
)
|
||||
events
|
||||
.forEach(event => {
|
||||
event.currentTarget = type
|
||||
})
|
||||
// we don't need to check for events.length
|
||||
// because we know it has at least one element
|
||||
callEventHandlerListeners(type._dEH, [events, transaction])
|
||||
})
|
||||
// only call afterTransaction listeners if anything changed
|
||||
transaction.afterState = getStates(transaction.y.store)
|
||||
// when all changes & events are processed, emit afterTransaction event
|
||||
// transaction cleanup
|
||||
const store = transaction.y.store
|
||||
const ds = transaction.deleteSet
|
||||
// replace deleted items with ItemDeleted / GC
|
||||
sortAndMergeDeleteSet(ds)
|
||||
y.emit('afterTransaction', [y, transaction])
|
||||
for (const [client, deleteItems] of ds.clients) {
|
||||
/**
|
||||
* @type {Array<AbstractStruct>}
|
||||
*/
|
||||
// @ts-ignore
|
||||
const structs = store.clients.get(client)
|
||||
for (let di = 0; di < deleteItems.length; di++) {
|
||||
const deleteItem = deleteItems[di]
|
||||
for (let si = findIndexSS(structs, deleteItem.clock); si < structs.length; si++) {
|
||||
const struct = structs[si]
|
||||
if (deleteItem.clock + deleteItem.len < struct.id.clock) {
|
||||
break
|
||||
}
|
||||
if (struct.deleted && struct instanceof AbstractItem && (struct.constructor !== ItemDeleted || (struct.parent._item !== null && struct.parent._item.deleted))) {
|
||||
// check if we can GC
|
||||
struct.gc(transaction, store)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {Array<AbstractStruct>} structs
|
||||
* @param {number} pos
|
||||
*/
|
||||
const tryToMergeWithLeft = (structs, pos) => {
|
||||
const left = structs[pos - 1]
|
||||
const right = structs[pos]
|
||||
if (left.deleted === right.deleted && left.constructor === right.constructor) {
|
||||
if (left.mergeWith(right)) {
|
||||
structs.splice(pos, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
// on all affected store.clients props, try to merge
|
||||
for (const [client, clock] of transaction.afterState) {
|
||||
const beforeClock = transaction.beforeState.get(client) || 0
|
||||
if (beforeClock !== clock) {
|
||||
/**
|
||||
* @type {Array<AbstractStruct>}
|
||||
*/
|
||||
// @ts-ignore
|
||||
const structs = 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--) {
|
||||
tryToMergeWithLeft(structs, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
// try to merge replacedItems
|
||||
for (const replacedItem of transaction._replacedItems) {
|
||||
const id = replacedItem.id
|
||||
const client = id.client
|
||||
const clock = id.clock
|
||||
/**
|
||||
* @type {Array<AbstractStruct>}
|
||||
*/
|
||||
// @ts-ignore
|
||||
const structs = store.clients.get(client)
|
||||
const replacedStructPos = findIndexSS(structs, clock)
|
||||
if (replacedStructPos + 1 < structs.length) {
|
||||
tryToMergeWithLeft(structs, replacedStructPos + 1)
|
||||
}
|
||||
if (replacedStructPos > 0) {
|
||||
tryToMergeWithLeft(structs, replacedStructPos)
|
||||
}
|
||||
}
|
||||
y.emit('afterTransactionCleanup', [y, transaction])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import {
|
||||
isParentOf,
|
||||
createID
|
||||
createID,
|
||||
transact
|
||||
} from '../internals.js'
|
||||
|
||||
class ReverseOperation {
|
||||
@@ -33,7 +34,7 @@ class ReverseOperation {
|
||||
function applyReverseOperation (y, scope, reverseBuffer) {
|
||||
let performedUndo = false
|
||||
let undoOp = null
|
||||
y.transact(() => {
|
||||
transact(y, () => {
|
||||
while (!performedUndo && reverseBuffer.length > 0) {
|
||||
undoOp = reverseBuffer.pop()
|
||||
// make sure that it is possible to iterate {from}-{to}
|
||||
@@ -107,7 +108,6 @@ export class UndoManager {
|
||||
this._lastTransactionWasUndo = false
|
||||
const y = scope._y
|
||||
this.y = y
|
||||
y._hasUndoManager = true
|
||||
let bindingInfos
|
||||
y.on('beforeTransaction', (y, transaction, remote) => {
|
||||
if (!remote) {
|
||||
|
||||
123
src/utils/Y.js
123
src/utils/Y.js
@@ -1,26 +1,18 @@
|
||||
import { getStates } from './StructStore.js'
|
||||
|
||||
import {
|
||||
callEventHandlerListeners,
|
||||
sortAndMergeDeleteSet,
|
||||
StructStore,
|
||||
findIndexSS,
|
||||
Transaction,
|
||||
AbstractType,
|
||||
AbstractItem,
|
||||
YArray,
|
||||
YText,
|
||||
YMap,
|
||||
YXmlFragment,
|
||||
ItemDeleted,
|
||||
YEvent, GC, AbstractStruct // eslint-disable-line
|
||||
transact,
|
||||
Transaction, YEvent // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import { Observable } from 'lib0/observable.js'
|
||||
import * as error from 'lib0/error.js'
|
||||
import * as random from 'lib0/random.js'
|
||||
import * as map from 'lib0/map.js'
|
||||
import * as math from 'lib0/math.js'
|
||||
|
||||
/**
|
||||
* A Yjs instance handles the state of shared data.
|
||||
@@ -43,7 +35,6 @@ export class Y extends Observable {
|
||||
* @type {Transaction | null}
|
||||
*/
|
||||
this._transaction = null
|
||||
this._hasUndoManager = false
|
||||
}
|
||||
/**
|
||||
* @type {Transaction}
|
||||
@@ -66,115 +57,7 @@ export class Y extends Observable {
|
||||
* @todo separate this into a separate function
|
||||
*/
|
||||
transact (f) {
|
||||
let initialCall = false
|
||||
if (this._transaction === null) {
|
||||
initialCall = true
|
||||
this._transaction = new Transaction(this)
|
||||
this.emit('beforeTransaction', [this, this._transaction])
|
||||
}
|
||||
try {
|
||||
f(this._transaction)
|
||||
} finally {
|
||||
if (initialCall) {
|
||||
const transaction = this._transaction
|
||||
this._transaction = null
|
||||
this.emit('beforeObserverCalls', [this, this._transaction])
|
||||
// emit change events on changed types
|
||||
transaction.changed.forEach((subs, itemtype) => {
|
||||
itemtype._callObserver(transaction, subs)
|
||||
})
|
||||
transaction.changedParentTypes.forEach((events, type) => {
|
||||
events = events
|
||||
.filter(event =>
|
||||
event.target._item === null || !event.target._item.deleted
|
||||
)
|
||||
events
|
||||
.forEach(event => {
|
||||
event.currentTarget = type
|
||||
})
|
||||
// we don't need to check for events.length
|
||||
// because we know it has at least one element
|
||||
callEventHandlerListeners(type._dEH, [events, transaction])
|
||||
})
|
||||
// only call afterTransaction listeners if anything changed
|
||||
transaction.afterState = getStates(transaction.y.store)
|
||||
// when all changes & events are processed, emit afterTransaction event
|
||||
// transaction cleanup
|
||||
const store = transaction.y.store
|
||||
const ds = transaction.deleteSet
|
||||
// replace deleted items with ItemDeleted / GC
|
||||
sortAndMergeDeleteSet(ds)
|
||||
this.emit('afterTransaction', [this, transaction])
|
||||
for (const [client, deleteItems] of ds.clients) {
|
||||
/**
|
||||
* @type {Array<AbstractStruct>}
|
||||
*/
|
||||
// @ts-ignore
|
||||
const structs = store.clients.get(client)
|
||||
for (let di = 0; di < deleteItems.length; di++) {
|
||||
const deleteItem = deleteItems[di]
|
||||
for (let si = findIndexSS(structs, deleteItem.clock); si < structs.length; si++) {
|
||||
const struct = structs[si]
|
||||
if (deleteItem.clock + deleteItem.len < struct.id.clock) {
|
||||
break
|
||||
}
|
||||
if (struct.deleted && struct instanceof AbstractItem && (struct.constructor !== ItemDeleted || (struct.parent._item !== null && struct.parent._item.deleted))) {
|
||||
// check if we can GC
|
||||
struct.gc(transaction, store)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {Array<AbstractStruct>} structs
|
||||
* @param {number} pos
|
||||
*/
|
||||
const tryToMergeWithLeft = (structs, pos) => {
|
||||
const left = structs[pos - 1]
|
||||
const right = structs[pos]
|
||||
if (left.deleted === right.deleted && left.constructor === right.constructor) {
|
||||
if (left.mergeWith(right)) {
|
||||
structs.splice(pos, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
// on all affected store.clients props, try to merge
|
||||
for (const [client, clock] of transaction.afterState) {
|
||||
const beforeClock = transaction.beforeState.get(client) || 0
|
||||
if (beforeClock !== clock) {
|
||||
/**
|
||||
* @type {Array<AbstractStruct>}
|
||||
*/
|
||||
// @ts-ignore
|
||||
const structs = 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--) {
|
||||
tryToMergeWithLeft(structs, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
// try to merge replacedItems
|
||||
for (const replacedItem of transaction._replacedItems) {
|
||||
const id = replacedItem.id
|
||||
const client = id.client
|
||||
const clock = id.clock
|
||||
/**
|
||||
* @type {Array<AbstractStruct>}
|
||||
*/
|
||||
// @ts-ignore
|
||||
const structs = store.clients.get(client)
|
||||
const replacedStructPos = findIndexSS(structs, clock)
|
||||
if (replacedStructPos + 1 < structs.length) {
|
||||
tryToMergeWithLeft(structs, replacedStructPos + 1)
|
||||
}
|
||||
if (replacedStructPos > 0) {
|
||||
tryToMergeWithLeft(structs, replacedStructPos)
|
||||
}
|
||||
}
|
||||
this.emit('afterTransactionCleanup', [this, transaction])
|
||||
}
|
||||
}
|
||||
transact(this, f)
|
||||
}
|
||||
/**
|
||||
* Define a shared data type.
|
||||
|
||||
@@ -61,8 +61,27 @@ export class RelativePosition {
|
||||
*/
|
||||
this.item = item
|
||||
}
|
||||
toJSON () {
|
||||
const json = {}
|
||||
if (this.type !== null) {
|
||||
json.type = this.type.toJSON()
|
||||
}
|
||||
if (this.tname !== null) {
|
||||
json.tname = this.tname
|
||||
}
|
||||
if (this.item !== null) {
|
||||
json.item = this.item.toJSON()
|
||||
}
|
||||
return json
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} json
|
||||
* @return {RelativePosition}
|
||||
*/
|
||||
export const createRelativePositionFromJSON = json => new RelativePosition(json.type == null ? null : createID(json.type.client, json.type.clock), json.tname || null, json.item == null ? null : createID(json.item.client, json.item.clock))
|
||||
|
||||
export class AbsolutePosition {
|
||||
/**
|
||||
* @param {AbstractType<any>} type
|
||||
@@ -175,11 +194,11 @@ export const readRelativePosition = (decoder, y, store) => {
|
||||
|
||||
/**
|
||||
* @param {RelativePosition} rpos
|
||||
* @param {StructStore} store
|
||||
* @param {Y} y
|
||||
* @return {AbsolutePosition|null}
|
||||
*/
|
||||
export const toAbsolutePosition = (rpos, store, y) => {
|
||||
export const toAbsolutePosition = (rpos, y) => {
|
||||
const store = y.store
|
||||
const rightID = rpos.item
|
||||
const typeID = rpos.type
|
||||
const tname = rpos.tname
|
||||
@@ -193,7 +212,7 @@ export const toAbsolutePosition = (rpos, store, y) => {
|
||||
if (!(right instanceof AbstractItem)) {
|
||||
return null
|
||||
}
|
||||
offset = right.deleted ? 0 : rightID.clock - right.id.clock
|
||||
offset = right.deleted || !right.countable ? 0 : rightID.clock - right.id.clock
|
||||
let n = right.left
|
||||
while (n !== null) {
|
||||
if (!n.deleted && n.countable) {
|
||||
@@ -251,9 +270,8 @@ export const toRelativePosition = (apos, y) => {
|
||||
* @param {RelativePosition|null} b
|
||||
*/
|
||||
export const compareRelativePositions = (a, b) => a === b || (
|
||||
a !== null && b !== null && (
|
||||
a !== null && b !== null && a.tname === b.tname && (
|
||||
(a.item !== null && b.item !== null && compareIDs(a.item, b.item)) ||
|
||||
(a.tname !== null && a.tname === b.tname) ||
|
||||
(a.type !== null && b.type !== null && compareIDs(a.type, b.type))
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user