rework and document api
This commit is contained in:
@@ -48,7 +48,7 @@ export class DeleteSet {
|
||||
/**
|
||||
* Iterate over all structs that were deleted.
|
||||
*
|
||||
* This function expects that the deletes structs are not deleted. Hence, you can
|
||||
* This function expects that the deletes structs are not merged. Hence, you can
|
||||
* probably only use it in type observes and `afterTransaction` events. But not
|
||||
* in `afterTransactionCleanup`.
|
||||
*
|
||||
@@ -266,6 +266,6 @@ export const readDeleteSet = (decoder, transaction, store) => {
|
||||
if (unappliedDS.clients.size > 0) {
|
||||
const unappliedDSEncoder = encoding.createEncoder()
|
||||
writeDeleteSet(unappliedDSEncoder, unappliedDS)
|
||||
store.pendingDeleteReaders.push(decoding.createDecoder(encoding.toBuffer(unappliedDSEncoder)))
|
||||
store.pendingDeleteReaders.push(decoding.createDecoder(encoding.toUint8Array(unappliedDSEncoder)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,26 +10,23 @@ import {
|
||||
YMap,
|
||||
YXmlFragment,
|
||||
transact,
|
||||
Transaction, YEvent // eslint-disable-line
|
||||
AbstractItem, Transaction, YEvent // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import { Observable } from 'lib0/observable.js'
|
||||
import * as random from 'lib0/random.js'
|
||||
import * as map from 'lib0/map.js'
|
||||
|
||||
// @todo rename to shared document
|
||||
|
||||
/**
|
||||
* A Yjs instance handles the state of shared data.
|
||||
* @extends Observable<string>
|
||||
*/
|
||||
export class Y extends Observable {
|
||||
export class Doc extends Observable {
|
||||
/**
|
||||
* @param {Object|undefined} conf configuration
|
||||
*/
|
||||
constructor (conf = {}) {
|
||||
super()
|
||||
// todo: change to clientId
|
||||
this.clientID = random.uint32()
|
||||
/**
|
||||
* @type {Map<string, AbstractType<YEvent>>}
|
||||
@@ -82,7 +79,7 @@ export class Y extends Observable {
|
||||
* }
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {Function} TypeConstructor The constructor of the type definition
|
||||
* @param {Function} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ...
|
||||
* @return {AbstractType<any>} The created type. Constructed with TypeConstructor
|
||||
*
|
||||
* @public
|
||||
@@ -97,9 +94,18 @@ export class Y extends Observable {
|
||||
const Constr = type.constructor
|
||||
if (TypeConstructor !== AbstractType && Constr !== TypeConstructor) {
|
||||
if (Constr === AbstractType) {
|
||||
const t = new Constr()
|
||||
// @ts-ignore
|
||||
const t = new TypeConstructor()
|
||||
t._map = type._map
|
||||
type._map.forEach(/** @param {AbstractItem?} n */ n => {
|
||||
for (; n !== null; n = n.left) {
|
||||
n.parent = t
|
||||
}
|
||||
})
|
||||
t._start = type._start
|
||||
for (let n = t._start; n !== null; n = n.right) {
|
||||
n.parent = t
|
||||
}
|
||||
t._length = type._length
|
||||
this.share.set(name, t)
|
||||
t._integrate(this, null)
|
||||
@@ -22,16 +22,6 @@ export class ID {
|
||||
*/
|
||||
this.clock = clock
|
||||
}
|
||||
/**
|
||||
* @deprecated
|
||||
* @todo remove and adapt relative position implementation
|
||||
*/
|
||||
toJSON () {
|
||||
return {
|
||||
client: this.client,
|
||||
clock: this.clock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,7 +81,7 @@ export const readID = decoder =>
|
||||
*/
|
||||
export const findRootTypeKey = type => {
|
||||
// @ts-ignore _y must be defined, otherwise unexpected case
|
||||
for (let [key, value] of type._y.share) {
|
||||
for (let [key, value] of type.doc.share) {
|
||||
if (value === type) {
|
||||
return key
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
/**
|
||||
* @module Cursors
|
||||
*/
|
||||
|
||||
import {
|
||||
getItem,
|
||||
@@ -13,7 +10,7 @@ import {
|
||||
findRootTypeKey,
|
||||
AbstractItem,
|
||||
ItemType,
|
||||
ID, StructStore, Y, AbstractType // eslint-disable-line
|
||||
ID, StructStore, Doc, AbstractType // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
@@ -21,29 +18,30 @@ import * as decoding from 'lib0/decoding.js'
|
||||
import * as error from 'lib0/error.js'
|
||||
|
||||
/**
|
||||
* A Cursor is a relative position that is based on the Yjs model. In contrast to an
|
||||
* absolute position (position by index), the Cursor can be
|
||||
* recomputed when remote changes are received. For example:
|
||||
* A relative position is based on the Yjs model and is not affected by document changes.
|
||||
* E.g. If you place a relative position before a certain character, it will always point to this character.
|
||||
* If you place a relative position at the end of a type, it will always point to the end of the type.
|
||||
*
|
||||
* ```Insert(0, 'x')('a|bc') = 'xa|bc'``` Where | is the cursor position.
|
||||
* A numeric position is often unsuited for user selections, because it does not change when content is inserted
|
||||
* before or after.
|
||||
*
|
||||
* A relative cursor position can be obtained with the function
|
||||
* ```Insert(0, 'x')('a|bc') = 'xa|bc'``` Where | is the relative position.
|
||||
*
|
||||
* One of the properties must be defined.
|
||||
*
|
||||
* @example
|
||||
* // Current cursor position is at position 10
|
||||
* const relativePosition = createCursorFromOffset(yText, 10)
|
||||
* const relativePosition = createRelativePositionFromIndex(yText, 10)
|
||||
* // modify yText
|
||||
* yText.insert(0, 'abc')
|
||||
* yText.delete(3, 10)
|
||||
* // Compute the cursor position
|
||||
* const absolutePosition = toAbsolutePosition(y, relativePosition)
|
||||
* const absolutePosition = createAbsolutePositionFromRelativePosition(y, relativePosition)
|
||||
* absolutePosition.type === yText // => true
|
||||
* console.log('cursor location is ' + absolutePosition.offset) // => cursor location is 3
|
||||
* console.log('cursor location is ' + absolutePosition.index) // => cursor location is 3
|
||||
*
|
||||
*/
|
||||
export class Cursor {
|
||||
export class RelativePosition {
|
||||
/**
|
||||
* @param {ID|null} type
|
||||
* @param {string|null} tname
|
||||
@@ -63,35 +61,22 @@ export class Cursor {
|
||||
*/
|
||||
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 {Cursor}
|
||||
* @return {RelativePosition}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const createCursorFromJSON = json => new Cursor(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 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
|
||||
* @param {number} offset
|
||||
* @param {number} index
|
||||
*/
|
||||
constructor (type, offset) {
|
||||
constructor (type, index) {
|
||||
/**
|
||||
* @type {AbstractType<any>}
|
||||
*/
|
||||
@@ -99,17 +84,17 @@ export class AbsolutePosition {
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
this.offset = offset
|
||||
this.index = index
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractType<any>} type
|
||||
* @param {number} offset
|
||||
* @param {number} index
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const createAbsolutePosition = (type, offset) => new AbsolutePosition(type, offset)
|
||||
export const createAbsolutePosition = (type, index) => new AbsolutePosition(type, index)
|
||||
|
||||
/**
|
||||
* @param {AbstractType<any>} type
|
||||
@@ -117,7 +102,7 @@ export const createAbsolutePosition = (type, offset) => new AbsolutePosition(typ
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const createCursor = (type, item) => {
|
||||
export const createRelativePosition = (type, item) => {
|
||||
let typeid = null
|
||||
let tname = null
|
||||
if (type._item === null) {
|
||||
@@ -125,40 +110,40 @@ export const createCursor = (type, item) => {
|
||||
} else {
|
||||
typeid = type._item.id
|
||||
}
|
||||
return new Cursor(typeid, tname, item)
|
||||
return new RelativePosition(typeid, tname, item)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a relativePosition based on a absolute position.
|
||||
*
|
||||
* @param {AbstractType<any>} type The base type (e.g. YText or YArray).
|
||||
* @param {number} offset The absolute position.
|
||||
* @return {Cursor}
|
||||
* @param {number} index The absolute position.
|
||||
* @return {RelativePosition}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const createCursorFromTypeOffset = (type, offset) => {
|
||||
export const createRelativePositionFromTypeIndex = (type, index) => {
|
||||
let t = type._start
|
||||
while (t !== null) {
|
||||
if (!t.deleted && t.countable) {
|
||||
if (t.length > offset) {
|
||||
if (t.length > index) {
|
||||
// case 1: found position somewhere in the linked list
|
||||
return createCursor(type, createID(t.id.client, t.id.clock + offset))
|
||||
return createRelativePosition(type, createID(t.id.client, t.id.clock + index))
|
||||
}
|
||||
offset -= t.length
|
||||
index -= t.length
|
||||
}
|
||||
t = t.right
|
||||
}
|
||||
return createCursor(type, null)
|
||||
return createRelativePosition(type, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {Cursor} rpos
|
||||
* @param {RelativePosition} rpos
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const writeCursor = (encoder, rpos) => {
|
||||
export const writeRelativePosition = (encoder, rpos) => {
|
||||
const { type, tname, item } = rpos
|
||||
if (item !== null) {
|
||||
encoding.writeVarUint(encoder, 0)
|
||||
@@ -177,15 +162,23 @@ export const writeCursor = (encoder, rpos) => {
|
||||
return encoder
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RelativePosition} rpos
|
||||
* @return {Uint8Array}
|
||||
*/
|
||||
export const encodeRelativePosition = rpos => {
|
||||
const encoder = encoding.createEncoder()
|
||||
writeRelativePosition(encoder, rpos)
|
||||
return encoding.toUint8Array(encoder)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {Y} y
|
||||
* @param {StructStore} store
|
||||
* @return {Cursor|null}
|
||||
* @return {RelativePosition|null}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const readCursor = (decoder, y, store) => {
|
||||
export const readRelativePosition = decoder => {
|
||||
let type = null
|
||||
let tname = null
|
||||
let itemID = null
|
||||
@@ -203,23 +196,29 @@ export const readCursor = (decoder, y, store) => {
|
||||
type = readID(decoder)
|
||||
}
|
||||
}
|
||||
return new Cursor(type, tname, itemID)
|
||||
return new RelativePosition(type, tname, itemID)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Cursor} cursor
|
||||
* @param {Y} y
|
||||
* @param {Uint8Array} uint8Array
|
||||
* @return {RelativePosition|null}
|
||||
*/
|
||||
export const decodeRelativePosition = uint8Array => readRelativePosition(decoding.createDecoder(uint8Array))
|
||||
|
||||
/**
|
||||
* @param {RelativePosition} rpos
|
||||
* @param {Doc} doc
|
||||
* @return {AbsolutePosition|null}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const createAbsolutePositionFromCursor = (cursor, y) => {
|
||||
const store = y.store
|
||||
const rightID = cursor.item
|
||||
const typeID = cursor.type
|
||||
const tname = cursor.tname
|
||||
export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
|
||||
const store = doc.store
|
||||
const rightID = rpos.item
|
||||
const typeID = rpos.type
|
||||
const tname = rpos.tname
|
||||
let type = null
|
||||
let offset = 0
|
||||
let index = 0
|
||||
if (rightID !== null) {
|
||||
if (getState(store, rightID.client) <= rightID.clock) {
|
||||
return null
|
||||
@@ -228,18 +227,18 @@ export const createAbsolutePositionFromCursor = (cursor, y) => {
|
||||
if (!(right instanceof AbstractItem)) {
|
||||
return null
|
||||
}
|
||||
offset = right.deleted || !right.countable ? 0 : rightID.clock - right.id.clock
|
||||
index = right.deleted || !right.countable ? 0 : rightID.clock - right.id.clock
|
||||
let n = right.left
|
||||
while (n !== null) {
|
||||
if (!n.deleted && n.countable) {
|
||||
offset += n.length
|
||||
index += n.length
|
||||
}
|
||||
n = n.left
|
||||
}
|
||||
type = right.parent
|
||||
} else {
|
||||
if (tname !== null) {
|
||||
type = y.get(tname)
|
||||
type = doc.get(tname)
|
||||
} else if (typeID !== null) {
|
||||
if (getState(store, typeID.client) <= typeID.clock) {
|
||||
// type does not exist yet
|
||||
@@ -255,20 +254,20 @@ export const createAbsolutePositionFromCursor = (cursor, y) => {
|
||||
} else {
|
||||
throw error.unexpectedCase()
|
||||
}
|
||||
offset = type._length
|
||||
index = type._length
|
||||
}
|
||||
if (type._item !== null && type._item.deleted) {
|
||||
return null
|
||||
}
|
||||
return createAbsolutePosition(type, offset)
|
||||
return createAbsolutePosition(type, index)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Cursor|null} a
|
||||
* @param {Cursor|null} b
|
||||
* @param {RelativePosition|null} a
|
||||
* @param {RelativePosition|null} b
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const compareCursors = (a, b) => a === b || (
|
||||
export const compareRelativePositions = (a, b) => a === b || (
|
||||
a !== null && b !== null && a.tname === b.tname && compareIDs(a.item, b.item) && compareIDs(a.type, b.type)
|
||||
)
|
||||
@@ -6,12 +6,10 @@ import {
|
||||
|
||||
export class Snapshot {
|
||||
/**
|
||||
* @param {DeleteSet} ds delete store
|
||||
* @param {DeleteSet} ds
|
||||
* @param {Map<number,number>} sm state map
|
||||
* @param {Map<number,string>} userMap
|
||||
* @private
|
||||
*/
|
||||
constructor (ds, sm, userMap) {
|
||||
constructor (ds, sm) {
|
||||
/**
|
||||
* @type {DeleteSet}
|
||||
* @private
|
||||
@@ -23,14 +21,15 @@ export class Snapshot {
|
||||
* @private
|
||||
*/
|
||||
this.sm = sm
|
||||
/**
|
||||
* @type {Map<number,string>}
|
||||
* @private
|
||||
*/
|
||||
this.userMap = userMap
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DeleteSet} ds
|
||||
* @param {Map<number,number>} sm
|
||||
*/
|
||||
export const createSnapshot = (ds, sm) => new Snapshot(ds, sm)
|
||||
|
||||
/**
|
||||
* @param {AbstractItem} item
|
||||
* @param {Snapshot|undefined} snapshot
|
||||
|
||||
@@ -6,8 +6,7 @@ import {
|
||||
|
||||
import * as math from 'lib0/math.js'
|
||||
import * as error from 'lib0/error.js'
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
import * as decoding from 'lib0/decoding.js' // eslint-disable-line
|
||||
|
||||
export class StructStore {
|
||||
constructor () {
|
||||
@@ -51,7 +50,7 @@ export class StructStore {
|
||||
* @public
|
||||
* @function
|
||||
*/
|
||||
export const getStates = store => {
|
||||
export const getStateVector = store => {
|
||||
const sm = new Map()
|
||||
store.clients.forEach((structs, client) => {
|
||||
const struct = structs[structs.length - 1]
|
||||
@@ -262,42 +261,3 @@ export const replaceStruct = (store, struct, newStruct) => {
|
||||
const structs = store.clients.get(struct.id.client)
|
||||
structs[findIndexSS(structs, struct.id.clock)] = newStruct
|
||||
}
|
||||
|
||||
/**
|
||||
* Read StateMap from Decoder and return as Map
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @return {Map<number,number>}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const readStatesAsMap = decoder => {
|
||||
const ss = new Map()
|
||||
const ssLength = decoding.readVarUint(decoder)
|
||||
for (let i = 0; i < ssLength; i++) {
|
||||
const client = decoding.readVarUint(decoder)
|
||||
const clock = decoding.readVarUint(decoder)
|
||||
ss.set(client, clock)
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
/**
|
||||
* Write StateMap to Encoder
|
||||
*
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {StructStore} store
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const writeStates = (encoder, store) => {
|
||||
encoding.writeVarUint(encoder, store.clients.size)
|
||||
store.clients.forEach((structs, client) => {
|
||||
const id = structs[structs.length - 1].id
|
||||
encoding.writeVarUint(encoder, id.client)
|
||||
encoding.writeVarUint(encoder, id.clock)
|
||||
})
|
||||
return encoder
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ import {
|
||||
writeDeleteSet,
|
||||
DeleteSet,
|
||||
sortAndMergeDeleteSet,
|
||||
getStates,
|
||||
getStateVector,
|
||||
findIndexSS,
|
||||
callEventHandlerListeners,
|
||||
AbstractItem,
|
||||
ID, AbstractType, AbstractStruct, YEvent, Y // eslint-disable-line
|
||||
ID, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
@@ -43,15 +43,15 @@ import * as math from 'lib0/math.js'
|
||||
*/
|
||||
export class Transaction {
|
||||
/**
|
||||
* @param {Y} y
|
||||
* @param {Doc} doc
|
||||
* @param {any} origin
|
||||
*/
|
||||
constructor (y, origin) {
|
||||
constructor (doc, origin) {
|
||||
/**
|
||||
* The Yjs instance.
|
||||
* @type {Y}
|
||||
* @type {Doc}
|
||||
*/
|
||||
this.y = y
|
||||
this.doc = doc
|
||||
/**
|
||||
* Describes the set of deleted items by ids
|
||||
* @type {DeleteSet}
|
||||
@@ -61,7 +61,7 @@ export class Transaction {
|
||||
* Holds the state before the transaction started.
|
||||
* @type {Map<Number,Number>}
|
||||
*/
|
||||
this.beforeState = getStates(y.store)
|
||||
this.beforeState = getStateVector(doc.store)
|
||||
/**
|
||||
* Holds the state after the transaction.
|
||||
* @type {Map<Number,Number>}
|
||||
@@ -80,11 +80,6 @@ export class Transaction {
|
||||
* @type {Map<AbstractType<YEvent>,Array<YEvent>>}
|
||||
*/
|
||||
this.changedParentTypes = new Map()
|
||||
/**
|
||||
* @type {encoding.Encoder|null}
|
||||
* @private
|
||||
*/
|
||||
this._updateMessage = null
|
||||
/**
|
||||
* @type {Set<ID>}
|
||||
* @private
|
||||
@@ -95,21 +90,20 @@ export class Transaction {
|
||||
*/
|
||||
this.origin = origin
|
||||
}
|
||||
/**
|
||||
* @type {encoding.Encoder|null}
|
||||
* @public
|
||||
*/
|
||||
get updateMessage () {
|
||||
// only create if content was added in transaction (state or ds changed)
|
||||
if (this._updateMessage === null && (this.deleteSet.clients.size > 0 || map.any(this.afterState, (clock, client) => this.beforeState.get(client) !== clock))) {
|
||||
const encoder = encoding.createEncoder()
|
||||
sortAndMergeDeleteSet(this.deleteSet)
|
||||
writeStructsFromTransaction(encoder, this)
|
||||
writeDeleteSet(encoder, this.deleteSet)
|
||||
this._updateMessage = encoder
|
||||
}
|
||||
return this._updateMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
export const computeUpdateMessageFromTransaction = transaction => {
|
||||
if (transaction.deleteSet.clients.size === 0 && !map.any(transaction.afterState, (clock, client) => transaction.beforeState.get(client) !== clock)) {
|
||||
return null
|
||||
}
|
||||
const encoder = encoding.createEncoder()
|
||||
sortAndMergeDeleteSet(transaction.deleteSet)
|
||||
writeStructsFromTransaction(encoder, transaction)
|
||||
writeDeleteSet(encoder, transaction.deleteSet)
|
||||
return encoder
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,45 +113,44 @@ export class Transaction {
|
||||
* @function
|
||||
*/
|
||||
export const nextID = transaction => {
|
||||
const y = transaction.y
|
||||
const y = transaction.doc
|
||||
return createID(y.clientID, getState(y.store, y.clientID))
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the functionality of `y.transact(()=>{..})`
|
||||
*
|
||||
* @param {Y} y
|
||||
* @param {Doc} doc
|
||||
* @param {function(Transaction):void} f
|
||||
* @param {any} [origin]
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const transact = (y, f, origin = null) => {
|
||||
const transactionCleanups = y._transactionCleanups
|
||||
export const transact = (doc, f, origin = null) => {
|
||||
const transactionCleanups = doc._transactionCleanups
|
||||
let initialCall = false
|
||||
if (y._transaction === null) {
|
||||
if (doc._transaction === null) {
|
||||
initialCall = true
|
||||
y._transaction = new Transaction(y, origin)
|
||||
transactionCleanups.push(y._transaction)
|
||||
y.emit('beforeTransaction', [y._transaction, y])
|
||||
doc._transaction = new Transaction(doc, origin)
|
||||
transactionCleanups.push(doc._transaction)
|
||||
doc.emit('beforeTransaction', [doc._transaction, doc])
|
||||
}
|
||||
try {
|
||||
f(y._transaction)
|
||||
f(doc._transaction)
|
||||
} finally {
|
||||
// @todo set after state here
|
||||
if (initialCall && transactionCleanups[0] === y._transaction) {
|
||||
if (initialCall && transactionCleanups[0] === doc._transaction) {
|
||||
// The first transaction ended, now process observer calls.
|
||||
// Observer call may create new transactions for which we need to call the observers and do cleanup.
|
||||
// We don't want to nest these calls, so we execute these calls one after another
|
||||
for (let i = 0; i < transactionCleanups.length; i++) {
|
||||
const transaction = transactionCleanups[i]
|
||||
const store = transaction.y.store
|
||||
const store = transaction.doc.store
|
||||
const ds = transaction.deleteSet
|
||||
sortAndMergeDeleteSet(ds)
|
||||
transaction.afterState = getStates(transaction.y.store)
|
||||
y._transaction = null
|
||||
y.emit('beforeObserverCalls', [transaction, y])
|
||||
transaction.afterState = getStateVector(transaction.doc.store)
|
||||
doc._transaction = null
|
||||
doc.emit('beforeObserverCalls', [transaction, doc])
|
||||
// emit change events on changed types
|
||||
transaction.changed.forEach((subs, itemtype) => {
|
||||
itemtype._callObserver(transaction, subs)
|
||||
@@ -175,7 +168,7 @@ export const transact = (y, f, origin = null) => {
|
||||
// because we know it has at least one element
|
||||
callEventHandlerListeners(type._dEH, events, transaction)
|
||||
})
|
||||
y.emit('afterTransaction', [transaction, y])
|
||||
doc.emit('afterTransaction', [transaction, doc])
|
||||
/**
|
||||
* @param {Array<AbstractStruct>} structs
|
||||
* @param {number} pos
|
||||
@@ -213,7 +206,7 @@ export const transact = (y, f, origin = null) => {
|
||||
break
|
||||
}
|
||||
if (struct.deleted && struct instanceof AbstractItem) {
|
||||
struct.gc(transaction, store, false)
|
||||
struct.gc(store, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -276,10 +269,15 @@ export const transact = (y, f, origin = null) => {
|
||||
}
|
||||
}
|
||||
// @todo Merge all the transactions into one and provide send the data as a single update message
|
||||
// @todo implement a dedicatet event that we can use to send updates to other peer
|
||||
y.emit('afterTransactionCleanup', [transaction, y])
|
||||
doc.emit('afterTransactionCleanup', [transaction, doc])
|
||||
if (doc._observers.has('update')) {
|
||||
const updateMessage = computeUpdateMessageFromTransaction(transaction)
|
||||
if (updateMessage !== null) {
|
||||
doc.emit('update', [encoding.toUint8Array(updateMessage), transaction.origin, doc])
|
||||
}
|
||||
}
|
||||
}
|
||||
y._transactionCleanups = []
|
||||
doc._transactionCleanups = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,10 +116,10 @@ export class UndoManager {
|
||||
this._undoing = false
|
||||
this._redoing = false
|
||||
this._lastTransactionWasUndo = false
|
||||
const y = scope._y
|
||||
this.y = y
|
||||
const doc = scope.doc
|
||||
this.y = doc
|
||||
let bindingInfos
|
||||
y.on('beforeTransaction', (y, transaction, remote) => {
|
||||
doc.on('beforeTransaction', (y, transaction, remote) => {
|
||||
if (!remote) {
|
||||
// Store binding information before transaction is executed
|
||||
// By restoring the binding information, we can make sure that the state
|
||||
@@ -130,7 +130,7 @@ export class UndoManager {
|
||||
})
|
||||
}
|
||||
})
|
||||
y.on('afterTransaction', (y, transaction, remote) => {
|
||||
doc.on('afterTransaction', (y, transaction, remote) => {
|
||||
if (!remote && transaction.changedParentTypes.has(scope)) {
|
||||
let reverseOperation = new ReverseOperation(y, transaction, bindingInfos)
|
||||
if (!this._undoing) {
|
||||
@@ -200,3 +200,4 @@ export class UndoManager {
|
||||
return performedRedo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@ import {
|
||||
createID,
|
||||
readID,
|
||||
getState,
|
||||
getStates,
|
||||
getStateVector,
|
||||
readDeleteSet,
|
||||
writeDeleteSet,
|
||||
createDeleteSetFromStructStore,
|
||||
Transaction, AbstractStruct, AbstractStructRef, StructStore, ID // eslint-disable-line
|
||||
Doc, Transaction, AbstractStruct, AbstractStructRef, StructStore, ID // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
@@ -104,7 +104,7 @@ export const writeClientsStructs = (encoder, store, _sm) => {
|
||||
sm.set(client, clock)
|
||||
}
|
||||
})
|
||||
getStates(store).forEach((clock, client) => {
|
||||
getStateVector(store).forEach((clock, client) => {
|
||||
if (!_sm.has(client)) {
|
||||
sm.set(client, 0)
|
||||
}
|
||||
@@ -250,7 +250,7 @@ export const tryResumePendingDeleteReaders = (transaction, store) => {
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const writeStructsFromTransaction = (encoder, transaction) => writeClientsStructs(encoder, transaction.y.store, transaction.beforeState)
|
||||
export const writeStructsFromTransaction = (encoder, transaction) => writeClientsStructs(encoder, transaction.doc.store, transaction.beforeState)
|
||||
|
||||
/**
|
||||
* @param {StructStore} store
|
||||
@@ -297,25 +297,127 @@ export const readStructs = (decoder, transaction, store) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and apply a document update.
|
||||
*
|
||||
* This function has the same effect as `applyUpdate` but accepts an decoder.
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @param {Doc} ydoc
|
||||
* @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))`
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const readModel = (decoder, transaction, store) => {
|
||||
readStructs(decoder, transaction, store)
|
||||
readDeleteSet(decoder, transaction, store)
|
||||
export const readUpdate = (decoder, ydoc, transactionOrigin) =>
|
||||
ydoc.transact(transaction => {
|
||||
readStructs(decoder, transaction, ydoc.store)
|
||||
readDeleteSet(decoder, transaction, ydoc.store)
|
||||
}, transactionOrigin)
|
||||
|
||||
/**
|
||||
* Apply a document update created by, for example, `y.on('update', update => ..)` or `update = encodeStateAsUpdate()`.
|
||||
*
|
||||
* This function has the same effect as `readUpdate` but accepts an Uint8Array instead of a Decoder.
|
||||
*
|
||||
* @param {Doc} ydoc
|
||||
* @param {Uint8Array} update
|
||||
* @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))`
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const applyUpdate = (ydoc, update, transactionOrigin) =>
|
||||
readUpdate(decoding.createDecoder(update), ydoc, transactionOrigin)
|
||||
|
||||
/**
|
||||
* Write all the document as a single update message. If you specify the state of the remote client (`targetStateVector`) it will
|
||||
* only write the operations that are missing.
|
||||
*
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {Doc} doc
|
||||
* @param {Map<number,number>} [targetStateVector] The state of the target that receives the update. Leave empty to write all known structs
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const writeStateAsUpdate = (encoder, doc, targetStateVector = new Map()) => {
|
||||
writeClientsStructs(encoder, doc.store, targetStateVector)
|
||||
writeDeleteSet(encoder, createDeleteSetFromStructStore(doc.store))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {StructStore} store
|
||||
* @param {Map<number,number>} [targetState] The state of the target that receives the update. Leave empty to write all known structs
|
||||
* Write all the document as a single update message that can be applied on the remote document. If you specify the state of the remote client (`targetState`) it will
|
||||
* only write the operations that are missing.
|
||||
*
|
||||
* Use `writeStateAsUpdate` instead if you are working with lib0/encoding.js#Encoder
|
||||
*
|
||||
* @param {Doc} doc
|
||||
* @param {Uint8Array} [encodedTargetStateVector] The state of the target that receives the update. Leave empty to write all known structs
|
||||
* @return {Uint8Array}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const writeModel = (encoder, store, targetState = new Map()) => {
|
||||
writeClientsStructs(encoder, store, targetState)
|
||||
writeDeleteSet(encoder, createDeleteSetFromStructStore(store))
|
||||
export const encodeStateAsUpdate = (doc, encodedTargetStateVector) => {
|
||||
const encoder = encoding.createEncoder()
|
||||
const targetStateVector = encodedTargetStateVector == null ? new Map() : decodeStateVector(encodedTargetStateVector)
|
||||
writeStateAsUpdate(encoder, doc, targetStateVector)
|
||||
return encoding.toUint8Array(encoder)
|
||||
}
|
||||
|
||||
/**
|
||||
* Read state vector from Decoder and return as Map
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @return {Map<number,number>} Maps `client` to the number next expected `clock` from that client.
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const readStateVector = decoder => {
|
||||
const ss = new Map()
|
||||
const ssLength = decoding.readVarUint(decoder)
|
||||
for (let i = 0; i < ssLength; i++) {
|
||||
const client = decoding.readVarUint(decoder)
|
||||
const clock = decoding.readVarUint(decoder)
|
||||
ss.set(client, clock)
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
/**
|
||||
* Read decodedState and return State as Map.
|
||||
*
|
||||
* @param {Uint8Array} decodedState
|
||||
* @return {Map<number,number>} Maps `client` to the number next expected `clock` from that client.
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const decodeStateVector = decodedState => readStateVector(decoding.createDecoder(decodedState))
|
||||
|
||||
/**
|
||||
* Write State Vector to `lib0/encoding.js#Encoder`.
|
||||
*
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {Doc} doc
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const writeDocumentStateVector = (encoder, doc) => {
|
||||
encoding.writeVarUint(encoder, doc.store.clients.size)
|
||||
doc.store.clients.forEach((structs, client) => {
|
||||
const id = structs[structs.length - 1].id
|
||||
encoding.writeVarUint(encoder, id.client)
|
||||
encoding.writeVarUint(encoder, id.clock)
|
||||
})
|
||||
return encoder
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode State as Uint8Array.
|
||||
*
|
||||
* @param {Doc} doc
|
||||
* @return {Uint8Array}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const encodeDocumentStateVector = doc => {
|
||||
const encoder = encoding.createEncoder()
|
||||
writeDocumentStateVector(encoder, doc)
|
||||
return encoding.toUint8Array(encoder)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user