refactoring: removed default connector and persistence, new code style, proper jsdocs, enabled typechecking

This commit is contained in:
Kevin Jahns
2018-10-29 21:58:21 +01:00
parent fe038822a3
commit e1ece6dc66
84 changed files with 3479 additions and 2580 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
import { createMutualExclude } from '../../lib/mutualExclude.js'
import { createMutex } from '../../lib/mutex.js'
/**
* Abstract class for bindings.
@@ -35,7 +35,7 @@ export default class Binding {
/**
* @private
*/
this._mutualExclude = createMutualExclude()
this._mutualExclude = createMutex()
}
/**
* Remove all data observers (both from the type and the target).
+8 -4
View File
@@ -8,6 +8,10 @@ import { defaultFilter, applyFilterOnType } from './filter.js'
import typeObserver from './typeObserver.js'
import domObserver from './domObserver.js'
/**
* @typedef {import('./filter.js').DomFilter} DomFilter
*/
/**
* A binding that binds the children of a YXmlFragment to a DOM element.
*
@@ -26,7 +30,7 @@ export default class DomBinding extends Binding {
* @param {Element} target The bind target. Mirrors the target.
* @param {Object} [opts] Optional configurations
* @param {FilterFunction} [opts.filter=defaultFilter] The filter function to use.
* @param {DomFilter} [opts.filter=defaultFilter] The filter function to use.
*/
constructor (type, target, opts = {}) {
// Binding handles textType as this.type and domTextarea as this.target
@@ -48,7 +52,7 @@ export default class DomBinding extends Binding {
/**
* Defines which DOM attributes and elements to filter out.
* Also filters remote changes.
* @type {FilterFunction}
* @type {DomFilter}
*/
this.filter = opts.filter || defaultFilter
// set initial value
@@ -57,7 +61,7 @@ export default class DomBinding extends Binding {
target.insertBefore(child.toDom(opts.document, opts.hooks, this), null)
})
this._typeObserver = typeObserver.bind(this)
this._domObserver = (mutations) => {
this._domObserver = mutations => {
domObserver.call(this, mutations, opts.document)
}
type.observeDeep(this._typeObserver)
@@ -119,7 +123,7 @@ export default class DomBinding extends Binding {
/**
* NOTE: currently does not apply filter to existing elements!
* @param {FilterFunction} filter The filter function to use from now on.
* @param {DomFilter} filter The filter function to use from now on.
*/
setFilter (filter) {
this.filter = filter
+46 -41
View File
@@ -1,60 +1,65 @@
/* eslint-env browser */
import YXmlText from '../../Types/YXml/YXmlText.js'
import YXmlHook from '../../Types/YXml/YXmlHook.js'
import YXmlElement from '../../Types/YXml/YXmlElement.js'
import { createAssociation, domsToTypes } from './util.js'
import { filterDomAttributes, defaultFilter } from './filter.js'
/**
* @typedef {import('./filter.js').DomFilter} DomFilter
* @typedef {import('./DomBinding.js').default} DomBinding
*/
/**
* Creates a Yjs type (YXml) based on the contents of a DOM Element.
*
* @param {Element|TextNode} element The DOM Element
* @param {Element|Text} element The DOM Element
* @param {?Document} _document Optional. Provide the global document object
* @param {Hooks} [hooks = {}] Optional. Set of Yjs Hooks
* @param {Filter} [filter=defaultFilter] Optional. Dom element filter
* @param {Object<string, any>} [hooks = {}] Optional. Set of Yjs Hooks
* @param {DomFilter} [filter=defaultFilter] Optional. Dom element filter
* @param {?DomBinding} binding Warning: This property is for internal use only!
* @return {YXmlElement | YXmlText}
* @return {YXmlElement | YXmlText | false}
*/
export default function domToType (element, _document = document, hooks = {}, filter = defaultFilter, binding) {
let type
switch (element.nodeType) {
case _document.ELEMENT_NODE:
let hookName = null
let hook
// configure `hookName !== undefined` if element is a hook.
if (element.hasAttribute('data-yjs-hook')) {
hookName = element.getAttribute('data-yjs-hook')
hook = hooks[hookName]
if (hook === undefined) {
console.error(`Unknown hook "${hookName}". Deleting yjsHook dataset property.`)
delete element.removeAttribute('data-yjs-hook')
hookName = null
}
/**
* @type {any}
*/
let type = null
if (element instanceof Element) {
let hookName = null
let hook
// configure `hookName !== undefined` if element is a hook.
if (element.hasAttribute('data-yjs-hook')) {
hookName = element.getAttribute('data-yjs-hook')
hook = hooks[hookName]
if (hook === undefined) {
console.error(`Unknown hook "${hookName}". Deleting yjsHook dataset property.`)
element.removeAttribute('data-yjs-hook')
hookName = null
}
if (hookName === null) {
// Not a hook
const attrs = filterDomAttributes(element, filter)
if (attrs === null) {
type = false
} else {
type = new YXmlElement(element.nodeName)
attrs.forEach((val, key) => {
type.setAttribute(key, val)
})
type.insert(0, domsToTypes(element.childNodes, document, hooks, filter, binding))
}
}
if (hookName === null) {
// Not a hook
const attrs = filterDomAttributes(element, filter)
if (attrs === null) {
type = false
} else {
// Is a hook
type = new YXmlHook(hookName)
hook.fillType(element, type)
type = new YXmlElement(element.nodeName)
attrs.forEach((val, key) => {
type.setAttribute(key, val)
})
type.insert(0, domsToTypes(element.childNodes, document, hooks, filter, binding))
}
break
case _document.TEXT_NODE:
type = new YXmlText()
type.insert(0, element.nodeValue)
break
default:
throw new Error('Can\'t transform this node type to a YXml type!')
} else {
// Is a hook
type = new YXmlHook(hookName)
hook.fillType(element, type)
}
} else if (element instanceof Text) {
type = new YXmlText()
type.insert(0, element.nodeValue)
} else {
throw new Error('Can\'t transform this node type to a YXml type!')
}
createAssociation(binding, element, type)
return type
+7
View File
@@ -1,5 +1,12 @@
import isParentOf from '../../Util/isParentOf.js'
/**
* @callback DomFilter
* @param {string} nodeName
* @param {Map<string, string>} attrs
* @return {Map | null}
*/
/**
* Default filter method (does nothing).
*
+1 -1
View File
@@ -17,7 +17,7 @@ function _getCurrentRelativeSelection (domBinding) {
return null
}
export const getCurrentRelativeSelection = typeof getSelection !== 'undefined' ? _getCurrentRelativeSelection : () => null
export const getCurrentRelativeSelection = typeof getSelection !== 'undefined' ? _getCurrentRelativeSelection : domBinding => null
export function beforeTransactionSelectionFixer (domBinding) {
relativeSelection = getCurrentRelativeSelection(domBinding)
+11 -4
View File
@@ -1,3 +1,4 @@
/* eslint-env browser */
/* global getSelection */
import YXmlText from '../../Types/YXml/YXmlText.js'
@@ -17,11 +18,17 @@ function findScrollReference (scrollingElement) {
}
}
} else {
if (anchor.nodeType === document.TEXT_NODE) {
anchor = anchor.parentElement
/**
* @type {Element}
*/
let elem = anchor.parentElement
if (anchor instanceof Element) {
elem = anchor
}
return {
elem,
top: elem.getBoundingClientRect().top
}
const top = anchor.getBoundingClientRect().top
return { elem: anchor, top: top }
}
}
return null
+9 -2
View File
@@ -1,6 +1,13 @@
import domToType from './domToType.js'
/**
* @typedef {import('../../Types/YXml/YXmlText.js').default} YXmlText
* @typedef {import('../../Types/YXml/YXmlElement.js').default} YXmlElement
* @typedef {import('../../Types/YXml/YXmlHook.js').default} YXmlHook
* @typedef {import('./DomBinding.js').default} DomBinding
*/
/**
* Iterates items until an undeleted item is found.
*
@@ -32,8 +39,8 @@ export function removeAssociation (domBinding, dom, type) {
* type).
*
* @param {DomBinding} domBinding The binding object
* @param {Element} dom The dom that is to be associated with type
* @param {YXmlElement|YXmlHook} type The type that is to be associated with dom
* @param {DocumentFragment|Element|Text} dom The dom that is to be associated with type
* @param {YXmlElement|YXmlHook|YXmlText} type The type that is to be associated with dom
*
*/
export function createAssociation (domBinding, dom, type) {
-32
View File
@@ -1,32 +0,0 @@
import { writeStructs } from './syncStep1.js'
import { integrateRemoteStructs } from './integrateRemoteStructs.js'
import { readDeleteSet, writeDeleteSet } from './deleteSet.js'
import BinaryEncoder from '../Util/Binary/Encoder.js'
/**
* Read the Decoder and fill the Yjs instance with data in the decoder.
*
* @param {Y} y The Yjs instance
* @param {BinaryDecoder} decoder The BinaryDecoder to read from.
*/
export function fromBinary (y, decoder) {
y.transact(function () {
integrateRemoteStructs(y, decoder)
readDeleteSet(y, decoder)
})
}
/**
* Encode the Yjs model to binary format.
*
* @param {Y} y The Yjs instance
* @return {BinaryEncoder} The encoder instance that can be transformed
* to ArrayBuffer or Buffer.
*/
export function toBinary (y) {
let encoder = new BinaryEncoder()
writeStructs(y, encoder, new Map())
writeDeleteSet(y, encoder)
return encoder
}
-130
View File
@@ -1,130 +0,0 @@
import { deleteItemRange } from '../Struct/Delete.js'
import ID from '../Util/ID/ID.js'
export function stringifyDeleteSet (y, decoder, strBuilder) {
let dsLength = decoder.readUint32()
for (let i = 0; i < dsLength; i++) {
let user = decoder.readVarUint()
strBuilder.push(' -' + user + ':')
let dvLength = decoder.readVarUint()
for (let j = 0; j < dvLength; j++) {
let from = decoder.readVarUint()
let len = decoder.readVarUint()
let gc = decoder.readUint8() === 1
strBuilder.push(`clock: ${from}, length: ${len}, gc: ${gc}`)
}
}
return strBuilder
}
export function writeDeleteSet (y, encoder) {
let currentUser = null
let currentLength
let lastLenPos
let numberOfUsers = 0
let laterDSLenPus = encoder.pos
encoder.writeUint32(0)
y.ds.iterate(null, null, function (n) {
var user = n._id.user
var clock = n._id.clock
var len = n.len
var gc = n.gc
if (currentUser !== user) {
numberOfUsers++
// a new user was found
if (currentUser !== null) { // happens on first iteration
encoder.setUint32(lastLenPos, currentLength)
}
currentUser = user
encoder.writeVarUint(user)
// pseudo-fill pos
lastLenPos = encoder.pos
encoder.writeUint32(0)
currentLength = 0
}
encoder.writeVarUint(clock)
encoder.writeVarUint(len)
encoder.writeUint8(gc ? 1 : 0)
currentLength++
})
if (currentUser !== null) { // happens on first iteration
encoder.setUint32(lastLenPos, currentLength)
}
encoder.setUint32(laterDSLenPus, numberOfUsers)
}
export function readDeleteSet (y, decoder) {
let dsLength = decoder.readUint32()
for (let i = 0; i < dsLength; i++) {
let user = decoder.readVarUint()
let dv = []
let dvLength = decoder.readUint32()
for (let j = 0; j < dvLength; j++) {
let from = decoder.readVarUint()
let len = decoder.readVarUint()
let gc = decoder.readUint8() === 1
dv.push([from, len, gc])
}
if (dvLength > 0) {
let pos = 0
let d = dv[pos]
let deletions = []
y.ds.iterate(new ID(user, 0), new ID(user, Number.MAX_VALUE), function (n) {
// cases:
// 1. d deletes something to the right of n
// => go to next n (break)
// 2. d deletes something to the left of n
// => create deletions
// => reset d accordingly
// *)=> if d doesn't delete anything anymore, go to next d (continue)
// 3. not 2) and d deletes something that also n deletes
// => reset d so that it doesn't contain n's deletion
// *)=> if d does not delete anything anymore, go to next d (continue)
while (d != null) {
var diff = 0 // describe the diff of length in 1) and 2)
if (n._id.clock + n.len <= d[0]) {
// 1)
break
} else if (d[0] < n._id.clock) {
// 2)
// delete maximum the len of d
// else delete as much as possible
diff = Math.min(n._id.clock - d[0], d[1])
// deleteItemRange(y, user, d[0], diff, true)
deletions.push([user, d[0], diff])
} else {
// 3)
diff = n._id.clock + n.len - d[0] // never null (see 1)
if (d[2] && !n.gc) {
// d marks as gc'd but n does not
// then delete either way
// deleteItemRange(y, user, d[0], Math.min(diff, d[1]), true)
deletions.push([user, d[0], Math.min(diff, d[1])])
}
}
if (d[1] <= diff) {
// d doesn't delete anything anymore
d = dv[++pos]
} else {
d[0] = d[0] + diff // reset pos
d[1] = d[1] - diff // reset length
}
}
})
// TODO: It would be more performant to apply the deletes in the above loop
// Adapt the Tree implementation to support delete while iterating
for (let i = deletions.length - 1; i >= 0; i--) {
const del = deletions[i]
deleteItemRange(y, del[0], del[1], del[2], true)
}
// for the rest.. just apply it
for (; pos < dv.length; pos++) {
d = dv[pos]
deleteItemRange(y, user, d[0], d[1], true)
// deletions.push([user, d[0], d[1], d[2]])
}
}
}
}
-65
View File
@@ -1,65 +0,0 @@
import BinaryDecoder from '../Util/Binary/Decoder.js'
import { stringifyStructs } from './integrateRemoteStructs.js'
import { stringifySyncStep1 } from './syncStep1.js'
import { stringifySyncStep2 } from './syncStep2.js'
import ID from '../Util/ID/ID.js'
import RootID from '../Util/ID/RootID.js'
import Y from '../Y.js'
export function messageToString ([y, buffer]) {
let decoder = new BinaryDecoder(buffer)
decoder.readVarString() // read roomname
let type = decoder.readVarString()
let strBuilder = []
strBuilder.push('\n === ' + type + ' ===')
if (type === 'update') {
stringifyStructs(y, decoder, strBuilder)
} else if (type === 'sync step 1') {
stringifySyncStep1(y, decoder, strBuilder)
} else if (type === 'sync step 2') {
stringifySyncStep2(y, decoder, strBuilder)
} else {
strBuilder.push('-- Unknown message type - probably an encoding issue!!!')
}
return strBuilder.join('\n')
}
export function messageToRoomname (buffer) {
let decoder = new BinaryDecoder(buffer)
decoder.readVarString() // roomname
return decoder.readVarString() // messageType
}
export function logID (id) {
if (id !== null && id._id != null) {
id = id._id
}
if (id === null) {
return '()'
} else if (id instanceof ID) {
return `(${id.user},${id.clock})`
} else if (id instanceof RootID) {
return `(${id.name},${id.type})`
} else if (id.constructor === Y) {
return `y`
} else {
throw new Error('This is not a valid ID!')
}
}
/**
* Helper utility to convert an item to a readable format.
*
* @param {String} name The name of the item class (YText, ItemString, ..).
* @param {Item} item The item instance.
* @param {String} [append] Additional information to append to the returned
* string.
* @return {String} A readable string that represents the item object.
*
* @private
*/
export function logItemHelper (name, item, append) {
const left = item._left !== null ? item._left._lastId : null
const origin = item._origin !== null ? item._origin._lastId : null
return `${name}(id:${logID(item._id)},left:${logID(left)},origin:${logID(origin)},right:${logID(item._right)},parent:${logID(item._parent)},parentSub:${item._parentSub}${append !== undefined ? ' - ' + append : ''})`
}
-23
View File
@@ -1,23 +0,0 @@
export function readStateSet (decoder) {
let ss = new Map()
let ssLength = decoder.readUint32()
for (let i = 0; i < ssLength; i++) {
let user = decoder.readVarUint()
let clock = decoder.readVarUint()
ss.set(user, clock)
}
return ss
}
export function writeStateSet (y, encoder) {
let lenPosition = encoder.pos
let len = 0
encoder.writeUint32(0)
for (let [user, clock] of y.ss.state) {
encoder.writeVarUint(user)
encoder.writeVarUint(clock)
len++
}
encoder.setUint32(lenPosition, len)
}
-83
View File
@@ -1,83 +0,0 @@
import BinaryEncoder from '../Util/Binary/Encoder.js'
import { readStateSet, writeStateSet } from './stateSet.js'
import { writeDeleteSet } from './deleteSet.js'
import ID from '../Util/ID/ID.js'
import { RootFakeUserID } from '../Util/ID/RootID.js'
export function stringifySyncStep1 (y, decoder, strBuilder) {
let auth = decoder.readVarString()
let protocolVersion = decoder.readVarUint()
strBuilder.push(` - auth: "${auth}"`)
strBuilder.push(` - protocolVersion: ${protocolVersion}`)
// write SS
let ssBuilder = []
let len = decoder.readUint32()
for (let i = 0; i < len; i++) {
let user = decoder.readVarUint()
let clock = decoder.readVarUint()
ssBuilder.push(`(${user}:${clock})`)
}
strBuilder.push(' == SS: ' + ssBuilder.join(','))
}
export function sendSyncStep1 (connector, syncUser) {
let encoder = new BinaryEncoder()
encoder.writeVarString(connector.y.room)
encoder.writeVarString('sync step 1')
encoder.writeVarString(connector.authInfo || '')
encoder.writeVarUint(connector.protocolVersion)
writeStateSet(connector.y, encoder)
connector.send(syncUser, encoder.createBuffer())
}
/**
* @private
* Write all Items that are not not included in ss to
* the encoder object.
*/
export function writeStructs (y, encoder, ss) {
const lenPos = encoder.pos
encoder.writeUint32(0)
let len = 0
for (let user of y.ss.state.keys()) {
let clock = ss.get(user) || 0
if (user !== RootFakeUserID) {
const minBound = new ID(user, clock)
const overlappingLeft = y.os.findPrev(minBound)
const rightID = overlappingLeft === null ? null : overlappingLeft._id
if (rightID !== null && rightID.user === user && rightID.clock + overlappingLeft._length > clock) {
const struct = overlappingLeft._clonePartial(clock - rightID.clock)
struct._toBinary(encoder)
len++
}
y.os.iterate(minBound, new ID(user, Number.MAX_VALUE), function (struct) {
struct._toBinary(encoder)
len++
})
}
}
encoder.setUint32(lenPos, len)
}
export function readSyncStep1 (decoder, encoder, y, senderConn, sender) {
let protocolVersion = decoder.readVarUint()
// check protocol version
if (protocolVersion !== y.connector.protocolVersion) {
console.warn(
`You tried to sync with a Yjs instance that has a different protocol version
(You: ${protocolVersion}, Client: ${protocolVersion}).
`)
y.destroy()
}
// write sync step 2
encoder.writeVarString('sync step 2')
encoder.writeVarString(y.connector.authInfo || '')
const ss = readStateSet(decoder)
writeStructs(y, encoder, ss)
writeDeleteSet(y, encoder)
y.connector.send(senderConn.uid, encoder.createBuffer())
senderConn.receivedSyncStep2 = true
if (y.connector.role === 'slave') {
sendSyncStep1(y.connector, sender)
}
}
-28
View File
@@ -1,28 +0,0 @@
import { stringifyStructs, integrateRemoteStructs } from './integrateRemoteStructs.js'
import { readDeleteSet } from './deleteSet.js'
export function stringifySyncStep2 (y, decoder, strBuilder) {
strBuilder.push(' - auth: ' + decoder.readVarString())
strBuilder.push(' == OS:')
stringifyStructs(y, decoder, strBuilder)
// write DS to string
strBuilder.push(' == DS:')
let len = decoder.readUint32()
for (let i = 0; i < len; i++) {
let user = decoder.readVarUint()
strBuilder.push(` User: ${user}: `)
let len2 = decoder.readUint32()
for (let j = 0; j < len2; j++) {
let from = decoder.readVarUint()
let to = decoder.readVarUint()
let gc = decoder.readUint8() === 1
strBuilder.push(`[${from}, ${to}, ${gc}]`)
}
}
}
export function readSyncStep2 (decoder, encoder, y, senderConn, sender) {
integrateRemoteStructs(y, decoder)
readDeleteSet(y, decoder)
y.connector._setSyncedWith(sender)
}
+8 -8
View File
@@ -1,8 +1,8 @@
import fs from 'fs'
import path from 'path'
import BinaryDecoder from '../Util/Binary/Decoder.js'
import BinaryEncoder from '../Util/Binary/Encoder.js'
import { createMutualExclude } from '../Util/mutualExclude.js'
import * as encoding from '../../lib/encoding.js'
import * as decoding from '../../lib/decoding.js'
import { createMutualExclude } from '../../lib/mutualExclude.js'
import { encodeUpdate, encodeStructsDS, decodePersisted } from './decodePersisted.js'
function createFilePath (persistence, roomName) {
@@ -23,9 +23,9 @@ export default class FilePersistence {
return new Promise((resolve, reject) => {
this._mutex(() => {
const filePath = createFilePath(this, room)
const updateMessage = new BinaryEncoder()
const updateMessage = encoding.createEncoder()
encodeUpdate(y, encodedStructs, updateMessage)
fs.appendFile(filePath, Buffer.from(updateMessage.createBuffer()), (err) => {
fs.appendFile(filePath, Buffer.from(encoding.toBuffer(updateMessage)), (err) => {
if (err !== null) {
reject(err)
} else {
@@ -37,10 +37,10 @@ export default class FilePersistence {
}
saveState (roomName, y) {
return new Promise((resolve, reject) => {
const encoder = new BinaryEncoder()
const encoder = encoding.createEncoder()
encodeStructsDS(y, encoder)
const filePath = createFilePath(this, roomName)
fs.writeFile(filePath, Buffer.from(encoder.createBuffer()), (err) => {
fs.writeFile(filePath, Buffer.from(encoding.toBuffer(encoder)), (err) => {
if (err !== null) {
reject(err)
} else {
@@ -61,7 +61,7 @@ export default class FilePersistence {
this._mutex(() => {
console.info(`unpacking data (${data.length})`)
console.time('unpacking')
decodePersisted(y, new BinaryDecoder(data))
decodePersisted(y, decoding.createDecoder(data.buffer))
console.timeEnd('unpacking')
})
resolve()
+4 -6
View File
@@ -1,12 +1,10 @@
/* global indexedDB, location, BroadcastChannel */
import Y from '../Y.js'
import { createMutualExclude } from '../Util/mutualExclude.js'
import { decodePersisted, encodeStructsDS, encodeUpdate } from './decodePersisted.js'
import BinaryDecoder from '../Util/Binary/Decoder.js'
import BinaryEncoder from '../Util/Binary/Encoder.js'
import { PERSIST_STRUCTS_DS } from './decodePersisted.js';
import { PERSIST_UPDATE } from './decodePersisted.js';
import { createMutualExclude } from '../../lib/mutualExclude.js'
import { decodePersisted, encodeStructsDS, encodeUpdate, PERSIST_STRUCTS_DS, PERSIST_UPDATE } from './decodePersisted.js'
import * as decoding from '../../lib/decoding.js'
import * as encoding from '../../lib/encoding.js'
/*
* Request to Promise transformer
*/
+2 -2
View File
@@ -39,10 +39,10 @@ export function decodePersisted (y, decoder) {
const contentType = decoder.readVarUint()
switch (contentType) {
case PERSIST_UPDATE:
integrateRemoteStructs(y, decoder)
integrateRemoteStructs(decoder, y)
break
case PERSIST_STRUCTS_DS:
integrateRemoteStructs(y, decoder)
integrateRemoteStructs(decoder, y)
readDeleteSet(y, decoder)
break
}
+6 -6
View File
@@ -1,6 +1,6 @@
import Tree from '../../lib/Tree.js'
import ID from '../Util/ID/ID.js'
import * as ID from '../Util/ID.js'
class DSNode {
constructor (id, len, gc) {
@@ -33,7 +33,7 @@ export default class DeleteStore extends Tree {
mark (id, length, gc) {
if (length === 0) return
// Step 1. Unmark range
const leftD = this.findWithUpperBound(new ID(id.user, id.clock - 1))
const leftD = this.findWithUpperBound(ID.createID(id.user, id.clock - 1))
// Resize left DSNode if necessary
if (leftD !== null && leftD._id.user === id.user) {
if (leftD._id.clock < id.clock && id.clock < leftD._id.clock + leftD.len) {
@@ -41,19 +41,19 @@ export default class DeleteStore extends Tree {
if (id.clock + length < leftD._id.clock + leftD.len) {
// overlaps new mark range and some more
// create another DSNode to the right of new mark
this.put(new DSNode(new ID(id.user, id.clock + length), leftD._id.clock + leftD.len - id.clock - length, leftD.gc))
this.put(new DSNode(ID.createID(id.user, id.clock + length), leftD._id.clock + leftD.len - id.clock - length, leftD.gc))
}
// resize left DSNode
leftD.len = id.clock - leftD._id.clock
} // Otherwise there is no overlapping
}
// Resize right DSNode if necessary
const upper = new ID(id.user, id.clock + length - 1)
const upper = ID.createID(id.user, id.clock + length - 1)
const rightD = this.findWithUpperBound(upper)
if (rightD !== null && rightD._id.user === id.user) {
if (rightD._id.clock < id.clock + length && id.clock <= rightD._id.clock && id.clock + length < rightD._id.clock + rightD.len) { // we only consider the case where we resize the node
const d = id.clock + length - rightD._id.clock
rightD._id = new ID(rightD._id.user, rightD._id.clock + d)
rightD._id = ID.createID(rightD._id.user, rightD._id.clock + d)
rightD.len -= d
}
}
@@ -72,7 +72,7 @@ export default class DeleteStore extends Tree {
leftD.len += length
newMark = leftD
}
const rightNext = this.find(new ID(id.user, id.clock + length))
const rightNext = this.find(ID.createID(id.user, id.clock + length))
if (rightNext !== null && rightNext._id.user === id.user && id.clock + length === rightNext._id.clock && gc === rightNext.gc) {
// We can merge newMark and rightNext
newMark.len += rightNext.len
+10 -10
View File
@@ -1,7 +1,7 @@
import Tree from '../../lib/Tree.js'
import RootID from '../Util/ID/RootID.js'
import * as ID from '../Util/ID.js'
import { getStruct } from '../Util/structReferences.js'
import { logID } from '../MessageHandler/messageToString.js'
import { stringifyID, stringifyItemID } from '../message.js'
import GC from '../Struct/GC.js'
export default class OperationStore extends Tree {
@@ -14,18 +14,18 @@ export default class OperationStore extends Tree {
this.iterate(null, null, function (item) {
if (item.constructor === GC) {
items.push({
id: logID(item),
id: stringifyItemID(item),
content: item._length,
deleted: 'GC'
})
} else {
items.push({
id: logID(item),
origin: logID(item._origin === null ? null : item._origin._lastId),
left: logID(item._left === null ? null : item._left._lastId),
right: logID(item._right),
right_origin: logID(item._right_origin),
parent: logID(item._parent),
id: stringifyItemID(item),
origin: item._origin === null ? '()' : stringifyID(item._origin._lastId),
left: item._left === null ? '()' : stringifyID(item._left._lastId),
right: stringifyItemID(item._right),
right_origin: stringifyItemID(item._right_origin),
parent: stringifyItemID(item._parent),
parentSub: item._parentSub,
deleted: item._deleted,
content: JSON.stringify(item._content)
@@ -36,7 +36,7 @@ export default class OperationStore extends Tree {
}
get (id) {
let struct = this.find(id)
if (struct === null && id instanceof RootID) {
if (struct === null && id instanceof ID.RootID) {
const Constr = getStruct(id.type)
const y = this.y
struct = new Constr()
+7 -3
View File
@@ -1,4 +1,8 @@
import ID from '../Util/ID/ID.js'
import * as ID from '../Util/ID.js'
/**
* @typedef {Map<number, number>} StateSet
*/
export default class StateStore {
constructor (y) {
@@ -18,14 +22,14 @@ export default class StateStore {
const user = this.y.userID
const state = this.getState(user)
this.setState(user, state + len)
return new ID(user, state)
return ID.createID(user, state)
}
updateRemoteState (struct) {
let user = struct._id.user
let userState = this.state.get(user)
while (struct !== null && struct._id.clock === userState) {
userState += struct._length
struct = this.y.os.get(new ID(user, userState))
struct = this.y.os.get(ID.createID(user, userState))
}
this.state.set(user, userState)
}
+31 -25
View File
@@ -1,31 +1,33 @@
import { getStructReference } from '../Util/structReferences.js'
import ID from '../Util/ID/ID.js'
import { logID } from '../MessageHandler/messageToString.js'
import * as ID from '../Util/ID.js'
import { stringifyID } from '../message.js'
import { writeStructToTransaction } from '../Util/Transaction.js'
import * as decoding from '../../lib/decoding.js'
import * as encoding from '../../lib/encoding.js'
/**
* @private
* Delete all items in an ID-range
* TODO: implement getItemCleanStartNode for better performance (only one lookup)
* Delete all items in an ID-range.
* Does not create delete operations!
* TODO: implement getItemCleanStartNode for better performance (only one lookup).
*/
export function deleteItemRange (y, user, clock, range, gcChildren) {
const createDelete = y.connector !== null && y.connector._forwardAppliedStructs
let item = y.os.getItemCleanStart(new ID(user, clock))
let item = y.os.getItemCleanStart(ID.createID(user, clock))
if (item !== null) {
if (!item._deleted) {
item._splitAt(y, range)
item._delete(y, createDelete, true)
item._delete(y, false, true)
}
let itemLen = item._length
range -= itemLen
clock += itemLen
if (range > 0) {
let node = y.os.findNode(new ID(user, clock))
while (node !== null && node.val !== null && range > 0 && node.val._id.equals(new ID(user, clock))) {
let node = y.os.findNode(ID.createID(user, clock))
while (node !== null && node.val !== null && range > 0 && node.val._id.equals(ID.createID(user, clock))) {
const nodeVal = node.val
if (!nodeVal._deleted) {
nodeVal._splitAt(y, range)
nodeVal._delete(y, createDelete, gcChildren)
nodeVal._delete(y, false, gcChildren)
}
const nodeLen = nodeVal._length
range -= nodeLen
@@ -44,6 +46,13 @@ export function deleteItemRange (y, user, clock, range, gcChildren) {
*/
export default class Delete {
constructor () {
/**
* @type {ID.ID}
*/
this._targetID = null
/**
* @type {import('./Item.js').default}
*/
this._target = null
this._length = null
}
@@ -54,15 +63,18 @@ export default class Delete {
*
* This is called when data is received from a remote peer.
*
* @param {Y} y The Yjs instance that this Item belongs to.
* @param {BinaryDecoder} decoder The decoder object to read data from.
* @param {import('../Y.js').default} y The Yjs instance that this Item belongs to.
* @param {decoding.Decoder} decoder The decoder object to read data from.
*/
_fromBinary (y, decoder) {
// TODO: set target, and add it to missing if not found
// There is an edge case in p2p networks!
const targetID = decoder.readID()
/**
* @type {any}
*/
const targetID = ID.decode(decoder)
this._targetID = targetID
this._length = decoder.readVarUint()
this._length = decoding.readVarUint(decoder)
if (y.os.getItem(targetID) === null) {
return [targetID]
} else {
@@ -77,12 +89,12 @@ export default class Delete {
*
* This is called when this Item is sent to a remote peer.
*
* @param {BinaryEncoder} encoder The encoder to write data to.
* @param {encoding.Encoder} encoder The encoder to write data to.
*/
_toBinary (encoder) {
encoder.writeUint8(getStructReference(this.constructor))
encoder.writeID(this._targetID)
encoder.writeVarUint(this._length)
encoding.writeUint8(encoder, getStructReference(this.constructor))
this._targetID.encode(encoder)
encoding.writeVarUint(encoder, this._length)
}
/**
@@ -102,12 +114,6 @@ export default class Delete {
// from remote
const id = this._targetID
deleteItemRange(y, id.user, id.clock, this._length, false)
} else if (y.connector !== null) {
// from local
y.connector.broadcastStruct(this)
}
if (y.persistence !== null) {
y.persistence.saveStruct(y, this)
}
writeStructToTransaction(y._transaction, this)
}
@@ -119,6 +125,6 @@ export default class Delete {
* @private
*/
_logString () {
return `Delete - target: ${logID(this._targetID)}, len: ${this._length}`
return `Delete - target: ${stringifyID(this._targetID)}, len: ${this._length}`
}
}
+20 -19
View File
@@ -1,11 +1,15 @@
import { getStructReference } from '../Util/structReferences.js'
import { RootFakeUserID } from '../Util/ID/RootID.js'
import ID from '../Util/ID/ID.js'
import * as ID from '../Util/ID.js'
import { writeStructToTransaction } from '../Util/Transaction.js'
import * as decoding from '../../lib/decoding.js'
import * as encoding from '../../lib/encoding.js'
// TODO should have the same base class as Item
export default class GC {
constructor () {
/**
* @type {ID.ID}
*/
this._id = null
this._length = 0
}
@@ -37,13 +41,7 @@ export default class GC {
n._length += next._length
y.os.delete(next._id)
}
if (id.user !== RootFakeUserID) {
if (y.connector !== null && (y.connector._forwardAppliedStructs || id.user === y.userID)) {
y.connector.broadcastStruct(this)
}
if (y.persistence !== null) {
y.persistence.saveStruct(y, this)
}
if (id.user !== ID.RootFakeUserID) {
writeStructToTransaction(y._transaction, this)
}
}
@@ -54,13 +52,13 @@ export default class GC {
*
* This is called when this Item is sent to a remote peer.
*
* @param {BinaryEncoder} encoder The encoder to write data to.
* @param {encoding.Encoder} encoder The encoder to write data to.
* @private
*/
_toBinary (encoder) {
encoder.writeUint8(getStructReference(this.constructor))
encoder.writeID(this._id)
encoder.writeVarUint(this._length)
encoding.writeUint8(encoder, getStructReference(this.constructor))
this._id.encode(encoder)
encoding.writeVarUint(encoder, this._length)
}
/**
@@ -68,17 +66,20 @@ export default class GC {
*
* This is called when data is received from a remote peer.
*
* @param {Y} y The Yjs instance that this Item belongs to.
* @param {BinaryDecoder} decoder The decoder object to read data from.
* @param {import('../Y.js').default} y The Yjs instance that this Item belongs to.
* @param {decoding.Decoder} decoder The decoder object to read data from.
* @private
*/
_fromBinary (y, decoder) {
const id = decoder.readID()
/**
* @type {any}
*/
const id = ID.decode(decoder)
this._id = id
this._length = decoder.readVarUint()
this._length = decoding.readVarUint(decoder)
const missing = []
if (y.ss.getState(id.user) < id.clock) {
missing.push(new ID(id.user, id.clock - 1))
missing.push(ID.createID(id.user, id.clock - 1))
}
return missing
}
@@ -89,7 +90,7 @@ export default class GC {
_clonePartial (diff) {
const gc = new GC()
gc._id = new ID(this._id.user, this._id.clock + diff)
gc._id = ID.createID(this._id.user, this._id.clock + diff)
gc._length = this._length - diff
return gc
}
+61 -56
View File
@@ -1,9 +1,15 @@
import { getStructReference } from '../Util/structReferences.js'
import ID from '../Util/ID/ID.js'
import { default as RootID, RootFakeUserID } from '../Util/ID/RootID.js'
import * as ID from '../Util/ID.js'
import Delete from './Delete.js'
import { transactionTypeChanged, writeStructToTransaction } from '../Util/Transaction.js'
import GC from './GC.js'
import * as encoding from '../../lib/encoding.js'
import * as decoding from '../../lib/decoding.js'
import Y from '../Y.js'
/**
* @typedef {import('./Type.js').default} YType
*/
/**
* @private
@@ -15,7 +21,7 @@ import GC from './GC.js'
*/
export function splitHelper (y, a, b, diff) {
const aID = a._id
b._id = new ID(aID.user, aID.clock + diff)
b._id = ID.createID(aID.user, aID.clock + diff)
b._origin = a
b._left = a
b._right = a._right
@@ -55,7 +61,7 @@ export default class Item {
constructor () {
/**
* The uniqe identifier of this type.
* @type {ID}
* @type {ID.ID | ID.RootID}
*/
this._id = null
/**
@@ -99,7 +105,7 @@ export default class Item {
/**
* If this type's effect is reundone this type refers to the type that undid
* this operation.
* @type {Item}
* @type {YType}
*/
this._redone = null
}
@@ -110,7 +116,8 @@ export default class Item {
* @private
*/
_copy () {
return new this.constructor()
const C = this.constructor
return C()
}
/**
@@ -124,6 +131,9 @@ export default class Item {
if (this._redone !== null) {
return this._redone
}
if (this._parent instanceof Y) {
return
}
let struct = this._copy()
let left, right
if (this._parentSub === null) {
@@ -146,7 +156,7 @@ export default class Item {
}
if (parent._redone !== null) {
parent = parent._redone
// find next cloned items
// find next cloned_redo items
while (left !== null) {
if (left._redone !== null && left._redone._parent === parent) {
left = left._redone
@@ -178,7 +188,11 @@ export default class Item {
* @private
*/
get _lastId () {
return new ID(this._id.user, this._id.clock + this._length - 1)
/**
* @type {any}
*/
const id = this._id
return ID.createID(id.user, id.clock + this._length - 1)
}
/**
@@ -227,10 +241,11 @@ export default class Item {
* @param {Y} y The Yjs instance
* @param {boolean} createDelete Whether to propagate a message that this
* Type was deleted.
* @param {boolean} gcChildren
*
* @private
*/
_delete (y, createDelete = true) {
_delete (y, createDelete = true, gcChildren) {
if (!this._deleted) {
this._deleted = true
y.ds.mark(this._id, this._length, false)
@@ -240,9 +255,6 @@ export default class Item {
if (createDelete) {
// broadcast and persists Delete
del._integrate(y, true)
} else if (y.persistence !== null) {
// only persist Delete
y.persistence.saveStruct(y, del)
}
transactionTypeChanged(y, this._parent, this._parentSub)
y._transaction.deletedStructs.add(this)
@@ -280,21 +292,30 @@ export default class Item {
* * Add this struct to y.os
* * Check if this is struct deleted
*
* @param {Y} y
*
* @private
*/
_integrate (y) {
y._transaction.newTypes.add(this)
/**
* @type {any}
*/
const parent = this._parent
/**
* @type {any}
*/
const selfID = this._id
const user = selfID === null ? y.userID : selfID.user
const userState = y.ss.getState(user)
if (selfID === null) {
this._id = y.ss.getNextID(this._length)
} else if (selfID.user === RootFakeUserID) {
// nop
} else if (selfID.user === ID.RootFakeUserID) {
// is parent
return
} else if (selfID.clock < userState) {
// already applied..
return []
return
} else if (selfID.clock === userState) {
y.ss.setState(selfID.user, userState + this._length)
} else {
@@ -304,7 +325,7 @@ export default class Item {
if (!parent._deleted && !y._transaction.changedTypes.has(parent) && !y._transaction.newTypes.has(parent)) {
// this is the first time parent is updated
// or this types is new
this._parent._beforeChange()
parent._beforeChange()
}
/*
@@ -328,9 +349,9 @@ export default class Item {
if (this._left !== null) {
o = this._left._right
} else if (this._parentSub !== null) {
o = this._parent._map.get(this._parentSub) || null
o = parent._map.get(this._parentSub) || null
} else {
o = this._parent._start
o = parent._start
}
let conflictingItems = new Set()
let itemsBeforeOrigin = new Set()
@@ -386,17 +407,11 @@ export default class Item {
}
}
if (parent._deleted) {
this._delete(y, false)
this._delete(y, false, true)
}
y.os.put(this)
transactionTypeChanged(y, parent, parentSub)
if (this._id.user !== RootFakeUserID) {
if (y.connector !== null && (y.connector._forwardAppliedStructs || this._id.user === y.userID)) {
y.connector.broadcastStruct(this)
}
if (y.persistence !== null) {
y.persistence.saveStruct(y, this)
}
if (this._id.user !== ID.RootFakeUserID) {
writeStructToTransaction(y._transaction, this)
}
}
@@ -407,12 +422,12 @@ export default class Item {
*
* This is called when this Item is sent to a remote peer.
*
* @param {BinaryEncoder} encoder The encoder to write data to.
* @param {encoding.Encoder} encoder The encoder to write data to.
*
* @private
*/
_toBinary (encoder) {
encoder.writeUint8(getStructReference(this.constructor))
encoding.writeUint8(encoder, getStructReference(this.constructor))
let info = 0
if (this._origin !== null) {
info += 0b1 // origin is defined
@@ -429,10 +444,10 @@ export default class Item {
if (this._parentSub !== null) {
info += 0b1000
}
encoder.writeUint8(info)
encoder.writeID(this._id)
encoding.writeUint8(encoder, info)
this._id.encode(encoder)
if (info & 0b1) {
encoder.writeID(this._origin._lastId)
this._origin._lastId.encode(encoder)
}
// TODO: remove
/* see above
@@ -441,14 +456,14 @@ export default class Item {
}
*/
if (info & 0b100) {
encoder.writeID(this._right_origin._id)
this._right_origin._id.encode(encoder)
}
if ((info & 0b101) === 0) {
// neither origin nor right is defined
encoder.writeID(this._parent._id)
this._parent._id.encode(encoder)
}
if (info & 0b1000) {
encoder.writeVarString(JSON.stringify(this._parentSub))
encoding.writeVarString(encoder, JSON.stringify(this._parentSub))
}
}
@@ -458,19 +473,19 @@ export default class Item {
* This is called when data is received from a remote peer.
*
* @param {Y} y The Yjs instance that this Item belongs to.
* @param {BinaryDecoder} decoder The decoder object to read data from.
* @param {decoding.Decoder} decoder The decoder object to read data from.
*
* @private
*/
_fromBinary (y, decoder) {
let missing = []
const info = decoder.readUint8()
const id = decoder.readID()
const info = decoding.readUint8(decoder)
const id = ID.decode(decoder)
this._id = id
// read origin
if (info & 0b1) {
// origin != null
const originID = decoder.readID()
const originID = ID.decode(decoder)
// we have to query for left again because it might have been split/merged..
const origin = y.os.getItemCleanEnd(originID)
if (origin === null) {
@@ -483,7 +498,7 @@ export default class Item {
// read right
if (info & 0b100) {
// right != null
const rightID = decoder.readID()
const rightID = ID.decode(decoder)
// we have to query for right again because it might have been split/merged..
const right = y.os.getItemCleanStart(rightID)
if (right === null) {
@@ -496,11 +511,11 @@ export default class Item {
// read parent
if ((info & 0b101) === 0) {
// neither origin nor right is defined
const parentID = decoder.readID()
const parentID = ID.decode(decoder)
// parent does not change, so we don't have to search for it again
if (this._parent === null) {
let parent
if (parentID.constructor === RootID) {
if (parentID.constructor === ID.RootID) {
parent = y.os.get(parentID)
} else {
parent = y.os.getItem(parentID)
@@ -513,27 +528,17 @@ export default class Item {
}
} else if (this._parent === null) {
if (this._origin !== null) {
if (this._origin.constructor === GC) {
// if origin is a gc, set parent also gc'd
this._parent = this._origin
} else {
this._parent = this._origin._parent
}
this._parent = this._origin._parent
} else if (this._right_origin !== null) {
// if origin is a gc, set parent also gc'd
if (this._right_origin.constructor === GC) {
this._parent = this._right_origin
} else {
this._parent = this._right_origin._parent
}
this._parent = this._right_origin._parent
}
}
if (info & 0b1000) {
// TODO: maybe put this in read parent condition (you can also read parentsub from left/right)
this._parentSub = JSON.parse(decoder.readVarString())
this._parentSub = JSON.parse(decoding.readVarString(decoder))
}
if (y.ss.getState(id.user) < id.clock) {
missing.push(new ID(id.user, id.clock - 1))
if (id instanceof ID.ID && y.ss.getState(id.user) < id.clock) {
missing.push(ID.createID(id.user, id.clock - 1))
}
return missing
}
+17 -4
View File
@@ -1,5 +1,11 @@
import Item from './Item.js'
import { logItemHelper } from '../MessageHandler/messageToString.js'
import { logItemHelper } from '../message.js'
import * as encoding from '../../lib/encoding.js'
import * as decoding from '../../lib/decoding.js'
/**
* @typedef {import('../index.js').Y} Y
*/
export default class ItemEmbed extends Item {
constructor () {
@@ -7,21 +13,28 @@ export default class ItemEmbed extends Item {
this.embed = null
}
_copy (undeleteChildren, copyPosition) {
let struct = super._copy(undeleteChildren, copyPosition)
let struct = super._copy()
struct.embed = this.embed
return struct
}
get _length () {
return 1
}
/**
* @param {Y} y
* @param {decoding.Decoder} decoder
*/
_fromBinary (y, decoder) {
const missing = super._fromBinary(y, decoder)
this.embed = JSON.parse(decoder.readVarString())
this.embed = JSON.parse(decoding.readVarString(decoder))
return missing
}
/**
* @param {encoding.Encoder} encoder
*/
_toBinary (encoder) {
super._toBinary(encoder)
encoder.writeVarString(JSON.stringify(this.embed))
encoding.writeVarString(encoder, JSON.stringify(this.embed))
}
/**
* Transform this YXml Type to a readable format.
+19 -6
View File
@@ -1,5 +1,11 @@
import Item from './Item.js'
import { logItemHelper } from '../MessageHandler/messageToString.js'
import { logItemHelper } from '../message.js'
import * as encoding from '../../lib/encoding.js'
import * as decoding from '../../lib/decoding.js'
/**
* @typedef {import('../index.js').Y} Y
*/
export default class ItemFormat extends Item {
constructor () {
@@ -8,7 +14,7 @@ export default class ItemFormat extends Item {
this.value = null
}
_copy (undeleteChildren, copyPosition) {
let struct = super._copy(undeleteChildren, copyPosition)
let struct = super._copy()
struct.key = this.key
struct.value = this.value
return struct
@@ -19,16 +25,23 @@ export default class ItemFormat extends Item {
get _countable () {
return false
}
/**
* @param {Y} y
* @param {decoding.Decoder} decoder
*/
_fromBinary (y, decoder) {
const missing = super._fromBinary(y, decoder)
this.key = decoder.readVarString()
this.value = JSON.parse(decoder.readVarString())
this.key = decoding.readVarString(decoder)
this.value = JSON.parse(decoding.readVarString(decoder))
return missing
}
/**
* @param {encoding.Encoder} encoder
*/
_toBinary (encoder) {
super._toBinary(encoder)
encoder.writeVarString(this.key)
encoder.writeVarString(JSON.stringify(this.value))
encoding.writeVarString(encoder, this.key)
encoding.writeVarString(encoder, JSON.stringify(this.value))
}
/**
* Transform this YXml Type to a readable format.
+18 -5
View File
@@ -1,5 +1,11 @@
import Item, { splitHelper } from './Item.js'
import { logItemHelper } from '../MessageHandler/messageToString.js'
import { logItemHelper } from '../message.js'
import * as encoding from '../../lib/encoding.js'
import * as decoding from '../../lib/decoding.js'
/**
* @typedef {import('../index.js').Y} Y
*/
export default class ItemJSON extends Item {
constructor () {
@@ -14,12 +20,16 @@ export default class ItemJSON extends Item {
get _length () {
return this._content.length
}
/**
* @param {Y} y
* @param {decoding.Decoder} decoder
*/
_fromBinary (y, decoder) {
let missing = super._fromBinary(y, decoder)
let len = decoder.readVarUint()
let len = decoding.readVarUint(decoder)
this._content = new Array(len)
for (let i = 0; i < len; i++) {
const ctnt = decoder.readVarString()
const ctnt = decoding.readVarString(decoder)
let parsed
if (ctnt === 'undefined') {
parsed = undefined
@@ -30,10 +40,13 @@ export default class ItemJSON extends Item {
}
return missing
}
/**
* @param {encoding.Encoder} encoder
*/
_toBinary (encoder) {
super._toBinary(encoder)
let len = this._content.length
encoder.writeVarUint(len)
encoding.writeVarUint(encoder, len)
for (let i = 0; i < len; i++) {
let encoded
let content = this._content[i]
@@ -42,7 +55,7 @@ export default class ItemJSON extends Item {
} else {
encoded = JSON.stringify(content)
}
encoder.writeVarString(encoded)
encoding.writeVarString(encoder, encoded)
}
}
/**
+16 -3
View File
@@ -1,5 +1,11 @@
import Item, { splitHelper } from './Item.js'
import { logItemHelper } from '../MessageHandler/messageToString.js'
import { logItemHelper } from '../message.js'
import * as encoding from '../../lib/encoding.js'
import * as decoding from '../../lib/decoding.js'
/**
* @typedef {import('../index.js').Y} Y
*/
export default class ItemString extends Item {
constructor () {
@@ -14,14 +20,21 @@ export default class ItemString extends Item {
get _length () {
return this._content.length
}
/**
* @param {Y} y
* @param {decoding.Decoder} decoder
*/
_fromBinary (y, decoder) {
let missing = super._fromBinary(y, decoder)
this._content = decoder.readVarString()
this._content = decoding.readVarString(decoder)
return missing
}
/**
* @param {encoding.Encoder} encoder
*/
_toBinary (encoder) {
super._toBinary(encoder)
encoder.writeVarString(this._content)
encoding.writeVarString(encoder, this._content)
}
/**
* Transform this YXml Type to a readable format.
+27 -5
View File
@@ -1,6 +1,11 @@
import Item from './Item.js'
import EventHandler from '../Util/EventHandler.js'
import ID from '../Util/ID/ID.js'
import { createID } from '../Util/ID.js'
import YEvent from '../Util/YEvent.js'
/**
* @typedef {import("../Y.js").default} Y
*/
// restructure children as if they were inserted one after another
function integrateChildren (y, start) {
@@ -22,7 +27,7 @@ export function getListItemIDByPosition (type, i) {
if (!n._deleted) {
if (pos <= i && i < pos + n._length) {
const id = n._id
return new ID(id.user, id.clock + i - pos)
return createID(id.user, id.clock + i - pos)
}
pos++
}
@@ -61,7 +66,7 @@ export default class Type extends Item {
* console.log(path) // might look like => [2, 'key1']
* child === type.get(path[0]).get(path[1])
*
* @param {YType} type Type target
* @param {Type | Y | any} type Type target
* @return {Array<string>} Path to the target
*/
getPathTo (type) {
@@ -91,6 +96,14 @@ export default class Type extends Item {
return path
}
/**
* @private
* Creates YArray Event and calls observers.
*/
_callObserver (transaction, parentSubs, remote) {
this._callEventHandler(transaction, new YEvent(this))
}
/**
* @private
* Call event listeners with an event. This will also add an event to all
@@ -99,6 +112,9 @@ export default class Type extends Item {
_callEventHandler (transaction, event) {
const changedParentTypes = transaction.changedParentTypes
this._eventHandler.callEventListeners(transaction, event)
/**
* @type {any}
*/
let type = this
while (type !== this._y) {
let events = changedParentTypes.get(type)
@@ -183,7 +199,7 @@ export default class Type extends Item {
this._start = null
integrateChildren(y, start)
}
// integrate map children
// integrate map children_integrate
const map = this._map
this._map = new Map()
for (let t of map.values()) {
@@ -206,6 +222,12 @@ export default class Type extends Item {
super._gc(y)
}
/**
* @abstract
* @return {Object | Array | number | string}
*/
toJSON () {}
/**
* @private
* Mark this Item as deleted.
@@ -213,7 +235,7 @@ export default class Type extends Item {
* @param {Y} y The Yjs instance
* @param {boolean} createDelete Whether to propagate a message that this
* Type was deleted.
* @param {boolean} [gcChildren=y._hasUndoManager===false] Whether to garbage
* @param {boolean} [gcChildren=(y._hasUndoManager===false)] Whether to garbage
* collect the children of this type.
*/
_delete (y, createDelete, gcChildren) {
+13 -11
View File
@@ -1,9 +1,15 @@
import Type from '../../Struct/Type.js'
import ItemJSON from '../../Struct/ItemJSON.js'
import ItemString from '../../Struct/ItemString.js'
import { logID, logItemHelper } from '../../MessageHandler/messageToString.js'
import { stringifyItemID, logItemHelper } from '../../message.js'
import YEvent from '../../Util/YEvent.js'
/**
* @typedef {import('../../Struct/Item.js').default} Item
* @typedef {import('../../Util/Transaction.js').default} Transaction
* @typedef {import('../../Y.js').default} Y
*/
/**
* Event that describes the changes on a YArray
*
@@ -76,7 +82,7 @@ export default class YArray extends Type {
/**
* Returns the i-th element from a YArray.
*
* @param {Integer} index The index of the element to return from the YArray
* @param {number} index The index of the element to return from the YArray
*/
get (index) {
let n = this._start
@@ -112,11 +118,7 @@ export default class YArray extends Type {
toJSON () {
return this.map(c => {
if (c instanceof Type) {
if (c.toJSON !== null) {
return c.toJSON()
} else {
return c.toString()
}
return c.toJSON()
}
return c
})
@@ -211,8 +213,8 @@ export default class YArray extends Type {
/**
* Deletes elements starting from an index.
*
* @param {Integer} index Index at which to start deleting elements
* @param {Integer} length The number of elements to remove. Defaults to 1.
* @param {number} index Index at which to start deleting elements
* @param {number} length The number of elements to remove. Defaults to 1.
*/
delete (index, length = 1) {
this._y.transact(() => {
@@ -318,7 +320,7 @@ export default class YArray extends Type {
* // Insert numbers 1, 2 at position 1
* yarray.insert(2, [1, 2])
*
* @param {Integer} index The index to insert content at.
* @param {number} index The index to insert content at.
* @param {Array} content The array of content
*/
insert (index, content) {
@@ -373,6 +375,6 @@ export default class YArray extends Type {
* @private
*/
_logString () {
return logItemHelper('YArray', this, `start:${logID(this._start)}"`)
return logItemHelper('YArray', this, `start:${stringifyItemID(this._start)}"`)
}
}
+6 -1
View File
@@ -1,9 +1,14 @@
import Item from '../../Struct/Item.js'
import Type from '../../Struct/Type.js'
import ItemJSON from '../../Struct/ItemJSON.js'
import { logItemHelper } from '../../MessageHandler/messageToString.js'
import { logItemHelper } from '../../message.js'
import YEvent from '../../Util/YEvent.js'
/**
* @typedef {import('../../Y.js').encodable} encodable
* @typedef {import('../../Struct/Type.js')} YType
*/
/**
* Event that describes the changes on a YMap.
*
+16 -8
View File
@@ -1,7 +1,7 @@
import ItemEmbed from '../../Struct/ItemEmbed.js'
import ItemString from '../../Struct/ItemString.js'
import ItemFormat from '../../Struct/ItemFormat.js'
import { logItemHelper } from '../../MessageHandler/messageToString.js'
import { logItemHelper } from '../../message.js'
import { YArrayEvent, default as YArray } from '../YArray/YArray.js'
/**
@@ -304,6 +304,9 @@ class YTextEvent extends YArrayEvent {
let deleteLen = 0
const addOp = function addOp () {
if (action !== null) {
/**
* @type {any}
*/
let op
switch (action) {
case 'delete':
@@ -483,6 +486,9 @@ export default class YText extends YArray {
*/
toString () {
let str = ''
/**
* @type {any}
*/
let n = this._start
while (n !== null) {
if (!n._deleted && n._countable) {
@@ -529,6 +535,9 @@ export default class YText extends YArray {
let ops = []
let currentAttributes = new Map()
let str = ''
/**
* @type {any}
*/
let n = this._start
function packStr () {
if (str.length > 0) {
@@ -568,12 +577,11 @@ export default class YText extends YArray {
/**
* Insert text at a given index.
*
* @param {Integer} index The index at which to start inserting.
* @param {number} index The index at which to start inserting.
* @param {String} text The text to insert at the specified position.
* @param {TextAttributes} attributes Optionally define some formatting
* information to apply on the inserted
* Text.
*
* @public
*/
insert (index, text, attributes = {}) {
@@ -589,7 +597,7 @@ export default class YText extends YArray {
/**
* Inserts an embed at a index.
*
* @param {Integer} index The index to insert the embed at.
* @param {number} index The index to insert the embed at.
* @param {Object} embed The Object that represents the embed.
* @param {TextAttributes} attributes Attribute information to apply on the
* embed
@@ -609,8 +617,8 @@ export default class YText extends YArray {
/**
* Deletes text starting from an index.
*
* @param {Integer} index Index at which to start deleting.
* @param {Integer} length The number of characters to remove. Defaults to 1.
* @param {number} index Index at which to start deleting.
* @param {number} length The number of characters to remove. Defaults to 1.
*
* @public
*/
@@ -627,8 +635,8 @@ export default class YText extends YArray {
/**
* Assigns properties to a range of text.
*
* @param {Integer} index The position where to start formatting.
* @param {Integer} length The amount of characters to assign properties to.
* @param {number} index The position where to start formatting.
* @param {number} length The amount of characters to assign properties to.
* @param {TextAttributes} attributes Attribute information to apply on the
* text.
*
+21 -9
View File
@@ -1,6 +1,12 @@
import YMap from '../YMap/YMap.js'
import YXmlFragment from './YXmlFragment.js'
import { createAssociation } from '../../Bindings/DomBinding/util.js'
import * as encoding from '../../../lib/encoding.js'
import * as decoding from '../../../lib/decoding.js'
/**
* @typedef {import('../../Y.js').default} Y
*/
/**
* An YXmlElement imitates the behavior of a
@@ -8,8 +14,6 @@ import { createAssociation } from '../../Bindings/DomBinding/util.js'
*
* * An YXmlElement has attributes (key value pairs)
* * An YXmlElement has childElements that must inherit from YXmlElement
*
* @param {String} nodeName Node name
*/
export default class YXmlElement extends YXmlFragment {
constructor (nodeName = 'UNDEFINED') {
@@ -34,11 +38,11 @@ export default class YXmlElement extends YXmlFragment {
* This is called when data is received from a remote peer.
*
* @param {Y} y The Yjs instance that this Item belongs to.
* @param {BinaryDecoder} decoder The decoder object to read data from.
* @param {decoding.Decoder} decoder The decoder object to read data from.
*/
_fromBinary (y, decoder) {
const missing = super._fromBinary(y, decoder)
this.nodeName = decoder.readVarString()
this.nodeName = decoding.readVarString(decoder)
return missing
}
@@ -48,13 +52,13 @@ export default class YXmlElement extends YXmlFragment {
*
* This is called when this Item is sent to a remote peer.
*
* @param {BinaryEncoder} encoder The encoder to write data to.
* @param {encoding.Encoder} encoder The encoder to write data to.
*
* @private
*/
_toBinary (encoder) {
super._toBinary(encoder)
encoder.writeVarString(this.nodeName)
encoding.writeVarString(encoder, this.nodeName)
}
/**
@@ -164,9 +168,9 @@ export default class YXmlElement extends YXmlFragment {
* @param {Document} [_document=document] The document object (you must define
* this when calling this method in
* nodejs)
* @param {Object<key:hookDefinition>} [hooks={}] Optional property to customize how hooks
* @param {Object<string, any>} [hooks={}] Optional property to customize how hooks
* are presented in the DOM
* @param {DomBinding} [binding] You should not set this property. This is
* @param {import('../../Bindings/DomBinding/DomBinding.js').default} [binding] You should not set this property. This is
* used if DomBinding wants to create a
* association to the created DOM type.
* @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
@@ -187,4 +191,12 @@ export default class YXmlElement extends YXmlFragment {
}
}
YXmlFragment._YXmlElement = YXmlElement
// reassign yxmlfragment to {any} type to prevent warnings
// assign yxmlelement to YXmlFragment so it has a reference to YXmlElement.
/**
* @type {any}
*/
const _reasgn = YXmlFragment
_reasgn._YXmlElement = YXmlElement
+5
View File
@@ -1,5 +1,10 @@
import YEvent from '../../Util/YEvent.js'
/**
* @typedef {import('../../Struct/Type.js').default} YType
* @typedef {import('../../Util/Transaction.js').default} Transaction
*/
/**
* An Event that describes changes on a YXml Element or Yxml Fragment
*
+10 -20
View File
@@ -3,7 +3,13 @@ import YXmlTreeWalker from './YXmlTreeWalker.js'
import YArray from '../YArray/YArray.js'
import YXmlEvent from './YXmlEvent.js'
import { logItemHelper } from '../../MessageHandler/messageToString.js'
import { logItemHelper } from '../../message.js'
/**
* @typedef {import('./YXmlElement.js').default} YXmlElement
* @typedef {import('../../Bindings/DomBinding/DomBinding.js').default} DomBinding
* @typedef {import('../../Y.js').default} Y
*/
/**
* Dom filter function.
@@ -48,7 +54,7 @@ export default class YXmlFragment extends YArray {
* @param {Function} filter Function that is called on each child element and
* returns a Boolean indicating whether the child
* is to be included in the subtree.
* @return {TreeWalker} A subtree and a position within it.
* @return {YXmlTreeWalker} A subtree and a position within it.
*
* @public
*/
@@ -67,7 +73,7 @@ export default class YXmlFragment extends YArray {
* - attribute
*
* @param {CSS_Selector} query The query on the children.
* @return {?YXmlElement} The first element that matches the query or null.
* @return {?import('./YXmlElement.js')} The first element that matches the query or null.
*
* @public
*/
@@ -116,29 +122,13 @@ export default class YXmlFragment extends YArray {
return this.map(xml => xml.toString()).join('')
}
/**
* @private
* Unbind from Dom and mark this Item as deleted.
*
* @param {Y} y The Yjs instance
* @param {boolean} createDelete Whether to propagate a message that this
* Type was deleted.
* @param {boolean} [gcChildren=y._hasUndoManager===false] Whether to garbage
* collect the children of this type.
*
* @private
*/
_delete (y, createDelete, gcChildren) {
super._delete(y, createDelete, gcChildren)
}
/**
* Creates a Dom Element that mirrors this YXmlElement.
*
* @param {Document} [_document=document] The document object (you must define
* this when calling this method in
* nodejs)
* @param {Object<key:hookDefinition>} [hooks={}] Optional property to customize how hooks
* @param {Object.<string, any>} [hooks={}] Optional property to customize how hooks
* are presented in the DOM
* @param {DomBinding} [binding] You should not set this property. This is
* used if DomBinding wants to create a
+12 -5
View File
@@ -1,5 +1,12 @@
import YMap from '../YMap/YMap.js'
import { createAssociation } from '../../Bindings/DomBinding/util.js'
import * as encoding from '../../../lib/encoding.js'
import * as decoding from '../../../lib/decoding.js'
/**
* @typedef {import('../../Bindings/DomBinding/DomBinding.js').default} DomBinding
* @typedef {import('../../Y.js').default} Y
*/
/**
* You can manage binding to a custom type with YXmlHook.
@@ -35,7 +42,7 @@ export default class YXmlHook extends YMap {
* @param {Document} [_document=document] The document object (you must define
* this when calling this method in
* nodejs)
* @param {Object<key:hookDefinition>} [hooks] Optional property to customize how hooks
* @param {Object.<string, any>} [hooks] Optional property to customize how hooks
* are presented in the DOM
* @param {DomBinding} [binding] You should not set this property. This is
* used if DomBinding wants to create a
@@ -63,13 +70,13 @@ export default class YXmlHook extends YMap {
* This is called when data is received from a remote peer.
*
* @param {Y} y The Yjs instance that this Item belongs to.
* @param {BinaryDecoder} decoder The decoder object to read data from.
* @param {decoding.Decoder} decoder The decoder object to read data from.
*
* @private
*/
_fromBinary (y, decoder) {
const missing = super._fromBinary(y, decoder)
this.hookName = decoder.readVarString()
this.hookName = decoding.readVarString(decoder)
return missing
}
@@ -79,13 +86,13 @@ export default class YXmlHook extends YMap {
*
* This is called when this Item is sent to a remote peer.
*
* @param {BinaryEncoder} encoder The encoder to write data to.
* @param {encoding.Encoder} encoder The encoder to write data to.
*
* @private
*/
_toBinary (encoder) {
super._toBinary(encoder)
encoder.writeVarString(this.hookName)
encoding.writeVarString(encoder, this.hookName)
}
/**
+7 -2
View File
@@ -1,6 +1,11 @@
import YText from '../YText/YText.js'
import { createAssociation } from '../../Bindings/DomBinding/util.js'
/**
* @typedef {import('../../Bindings/DomBinding/DomBinding.js').default} DomBinding
* @typedef {import('../../index.js').Y} Y
*/
/**
* Represents text in a Dom Element. In the future this type will also handle
* simple formatting information like bold and italic.
@@ -14,12 +19,12 @@ export default class YXmlText extends YText {
* @param {Document} [_document=document] The document object (you must define
* this when calling this method in
* nodejs)
* @param {Object<key:hookDefinition>} [hooks] Optional property to customize how hooks
* @param {Object<string, any>} [hooks] Optional property to customize how hooks
* are presented in the DOM
* @param {DomBinding} [binding] You should not set this property. This is
* used if DomBinding wants to create a
* association to the created DOM type.
* @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
* @return {Text} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
*
* @public
*/
+1 -1
View File
@@ -33,7 +33,7 @@ export default class YXmlTreeWalker {
/**
* Get the next node.
*
* @return {YXmlElement} The next node.
* @return {import('./YXmlElement.js').default} The next node.
*
* @public
*/
+90
View File
@@ -0,0 +1,90 @@
import { getStructReference } from './structReferences.js'
import * as decoding from '../../lib/decoding.js'
import * as encoding from '../../lib/encoding.js'
export class ID {
constructor (user, clock) {
this.user = user // TODO: rename to client
this.clock = clock
}
clone () {
return new ID(this.user, this.clock)
}
equals (id) {
return id !== null && id.user === this.user && id.clock === this.clock
}
lessThan (id) {
if (id.constructor === ID) {
return this.user < id.user || (this.user === id.user && this.clock < id.clock)
} else {
return false
}
}
/**
* @param {encoding.Encoder} encoder
*/
encode (encoder) {
encoding.writeVarUint(encoder, this.user)
encoding.writeVarUint(encoder, this.clock)
}
}
export const createID = (user, clock) => new ID(user, clock)
export const RootFakeUserID = 0xFFFFFF
export class RootID {
constructor (name, typeConstructor) {
this.user = RootFakeUserID
this.name = name
this.type = getStructReference(typeConstructor)
}
equals (id) {
return id !== null && id.user === this.user && id.name === this.name && id.type === this.type
}
lessThan (id) {
if (id.constructor === RootID) {
return this.user < id.user || (this.user === id.user && (this.name < id.name || (this.name === id.name && this.type < id.type)))
} else {
return true
}
}
/**
* @param {encoding.Encoder} encoder
*/
encode (encoder) {
encoding.writeVarUint(encoder, this.user)
encoding.writeVarString(encoder, this.name)
encoding.writeVarUint(encoder, this.type)
}
}
/**
* Create a new root id.
*
* @example
* y.define('name', Y.Array) // name, and typeConstructor
*
* @param {string} name
* @param {Function} typeConstructor must be defined in structReferences
*/
export const createRootID = (name, typeConstructor) => new RootID(name, typeConstructor)
/**
* Read ID.
* * If first varUint read is 0xFFFFFF a RootID is returned.
* * Otherwise an ID is returned
*
* @param {decoding.Decoder} decoder
* @return {ID|RootID}
*/
export const decode = decoder => {
const user = decoding.readVarUint(decoder)
if (user === RootFakeUserID) {
// read property name and type id
const rid = createRootID(decoding.readVarString(decoder), null)
rid.type = decoding.readVarUint(decoder)
return rid
}
return createID(user, decoding.readVarUint(decoder))
}
-20
View File
@@ -1,20 +0,0 @@
export default class ID {
constructor (user, clock) {
this.user = user // TODO: rename to client
this.clock = clock
}
clone () {
return new ID(this.user, this.clock)
}
equals (id) {
return id !== null && id.user === this.user && id.clock === this.clock
}
lessThan (id) {
if (id.constructor === ID) {
return this.user < id.user || (this.user === id.user && this.clock < id.clock)
} else {
return false
}
}
}
-21
View File
@@ -1,21 +0,0 @@
import { getStructReference } from '../structReferences.js'
export const RootFakeUserID = 0xFFFFFF
export default class RootID {
constructor (name, typeConstructor) {
this.user = RootFakeUserID
this.name = name
this.type = getStructReference(typeConstructor)
}
equals (id) {
return id !== null && id.user === this.user && id.name === this.name && id.type === this.type
}
lessThan (id) {
if (id.constructor === RootID) {
return this.user < id.user || (this.user === id.user && (this.name < id.name || (this.name === id.name && this.type < id.type)))
} else {
return true
}
}
}
+10 -4
View File
@@ -1,4 +1,10 @@
import BinaryEncoder from './Binary/Encoder.js'
import * as encoding from '../../lib/encoding.js'
/**
* @typedef {import("../Y.js").default} Y
* @typedef {import("../Struct/Type.js").default} YType
* @typedef {import("../Struct/Item.js").default} Item
* @typedef {import("./YEvent.js").default} YEvent
*/
/**
* A transaction is created for every change on the Yjs model. It is possible
@@ -26,7 +32,7 @@ import BinaryEncoder from './Binary/Encoder.js'
export default class Transaction {
constructor (y) {
/**
* @type {Y} The Yjs instance.
* @type {import("../Y.js")} The Yjs instance.
*/
this.y = y
/**
@@ -38,7 +44,7 @@ export default class Transaction {
* All types that were directly modified (property added or child
* inserted/deleted). New types are not included in this Set.
* Maps from type to parentSubs (`item._parentSub = null` for YArray)
* @type {Set<YType,String>}
* @type {Map<YType|Y,String>}
*/
this.changedTypes = new Map()
// TODO: rename deletedTypes
@@ -60,7 +66,7 @@ export default class Transaction {
*/
this.changedParentTypes = new Map()
this.encodedStructsLen = 0
this.encodedStructs = new BinaryEncoder()
this.encodedStructs = encoding.createEncoder()
}
}
+6 -6
View File
@@ -1,4 +1,4 @@
import ID from './ID/ID.js'
import * as ID from './ID.js'
import isParentOf from './isParentOf.js'
class ReverseOperation {
@@ -6,8 +6,8 @@ class ReverseOperation {
this.created = new Date()
const beforeState = transaction.beforeState
if (beforeState.has(y.userID)) {
this.toState = new ID(y.userID, y.ss.getState(y.userID) - 1)
this.fromState = new ID(y.userID, beforeState.get(y.userID))
this.toState = ID.createID(y.userID, y.ss.getState(y.userID) - 1)
this.fromState = ID.createID(y.userID, beforeState.get(y.userID))
} else {
this.toState = null
this.fromState = null
@@ -28,7 +28,7 @@ class ReverseOperation {
function applyReverseOperation (y, scope, reverseBuffer) {
let performedUndo = false
let undoOp
let undoOp = null
y.transact(() => {
while (!performedUndo && reverseBuffer.length > 0) {
undoOp = reverseBuffer.pop()
@@ -49,7 +49,7 @@ function applyReverseOperation (y, scope, reverseBuffer) {
const redoitems = new Set()
for (let del of undoOp.deletedStructs) {
const fromState = del.from
const toState = new ID(fromState.user, fromState.clock + del.len - 1)
const toState = ID.createID(fromState.user, fromState.clock + del.len - 1)
y.os.getItemCleanStart(fromState)
y.os.getItemCleanEnd(toState)
y.os.iterate(fromState, toState, op => {
@@ -73,7 +73,7 @@ function applyReverseOperation (y, scope, reverseBuffer) {
})
}
})
if (performedUndo) {
if (performedUndo && undoOp !== null) {
// should be performed after the undo transaction
undoOp.bindingInfos.forEach((info, binding) => {
binding._restoreUndoStackInfo(info)
+5
View File
@@ -1,3 +1,8 @@
/**
* @typedef {import("../Y.js").default} Y
* @typedef {import("../Struct/Type.js").default} YType
* @typedef {import("../Struct/Item.js").default} Item
*/
/**
* YEvent describes the changes on a YType.
+2 -2
View File
@@ -1,5 +1,5 @@
import ID from '../Util/ID/ID.js'
import * as ID from '../Util/ID.js'
import ItemJSON from '../Struct/ItemJSON.js'
import ItemString from '../Struct/ItemString.js'
@@ -32,7 +32,7 @@ export function defragmentItemContent (y) {
a.constructor === b.constructor &&
a._deleted === b._deleted &&
a._right === b &&
(new ID(a._id.user, a._id.clock + a._length)).equals(b._id)
(ID.createID(a._id.user, a._id.clock + a._length)).equals(b._id)
) {
a._right = b._right
if (a instanceof ItemJSON) {
+1 -1
View File
@@ -1,7 +1,7 @@
/* global crypto */
export function generateRandomUint32 () {
if (typeof crypto !== 'undefined' && crypto.getRandomValue != null) {
if (typeof crypto !== 'undefined' && crypto.getRandomValues != null) {
// browser
let arr = new Uint32Array(1)
crypto.getRandomValues(arr)
@@ -1,8 +1,12 @@
import { getStruct } from '../Util/structReferences.js'
import BinaryDecoder from '../Util/Binary/Decoder.js'
import { logID } from './messageToString.js'
import * as decoding from '../../lib/decoding.js'
import GC from '../Struct/GC.js'
/**
* @typedef {import('../index').Y} Y
* @typedef {import('../Struct/Item.js').default} YItem
*/
class MissingEntry {
constructor (decoder, missing, struct) {
this.decoder = decoder
@@ -16,6 +20,8 @@ class MissingEntry {
* Integrate remote struct
* When a remote struct is integrated, other structs might be ready to ready to
* integrate.
* @param {Y} y
* @param {YItem} struct
*/
function _integrateRemoteStructHelper (y, struct) {
const id = struct._id
@@ -57,29 +63,21 @@ function _integrateRemoteStructHelper (y, struct) {
msu.delete(clock)
}
}
if (msu.size === 0) {
y._missingStructs.delete(id.user)
}
}
}
}
export function stringifyStructs (y, decoder, strBuilder) {
const len = decoder.readUint32()
/**
* @param {decoding.Decoder} decoder
* @param {Y} y
*/
export function integrateRemoteStructs (decoder, y) {
const len = decoding.readUint32(decoder)
for (let i = 0; i < len; i++) {
let reference = decoder.readVarUint()
let Constr = getStruct(reference)
let struct = new Constr()
let missing = struct._fromBinary(y, decoder)
let logMessage = ' ' + struct._logString()
if (missing.length > 0) {
logMessage += ' .. missing: ' + missing.map(logID).join(', ')
}
strBuilder.push(logMessage)
}
}
export function integrateRemoteStructs (y, decoder) {
const len = decoder.readUint32()
for (let i = 0; i < len; i++) {
let reference = decoder.readVarUint()
let reference = decoding.readVarUint(decoder)
let Constr = getStruct(reference)
let struct = new Constr()
let decoderPos = decoder.pos
@@ -90,7 +88,7 @@ export function integrateRemoteStructs (y, decoder) {
struct = y._readyToIntegrate.shift()
}
} else {
let _decoder = new BinaryDecoder(decoder.uint8arr)
let _decoder = decoding.createDecoder(decoder.arr.buffer)
_decoder.pos = decoderPos
let missingEntry = new MissingEntry(_decoder, missing, struct)
let missingStructs = y._missingStructs
@@ -111,8 +109,12 @@ export function integrateRemoteStructs (y, decoder) {
}
// TODO: use this above / refactor
export function integrateRemoteStruct (y, decoder) {
let reference = decoder.readVarUint()
/**
* @param {decoding.Decoder} decoder
* @param {Y} y
*/
export function integrateRemoteStruct (decoder, y) {
let reference = decoding.readVarUint(decoder)
let Constr = getStruct(reference)
let struct = new Constr()
let decoderPos = decoder.pos
@@ -123,7 +125,7 @@ export function integrateRemoteStruct (y, decoder) {
struct = y._readyToIntegrate.shift()
}
} else {
let _decoder = new BinaryDecoder(decoder.uint8arr)
let _decoder = decoding.createDecoder(decoder.arr.buffer)
_decoder.pos = decoderPos
let missingEntry = new MissingEntry(_decoder, missing, struct)
let missingStructs = y._missingStructs
+7 -2
View File
@@ -1,9 +1,14 @@
/**
* @typedef {import('../Struct/Type.js').default} YType
* @typedef {import('../Y.js').default} Y
*/
/**
* Check if `parent` is a parent of `child`.
*
* @param {Type} parent
* @param {Type} child
* @param {YType | Y} parent
* @param {YType | Y} child
* @return {Boolean} Whether `parent` is a parent of `child`.
*
* @public
+4 -5
View File
@@ -1,5 +1,4 @@
import ID from './ID/ID.js'
import RootID from './ID/RootID.js'
import * as ID from './ID.js'
import GC from '../Struct/GC.js'
// TODO: Implement function to describe ranges
@@ -72,9 +71,9 @@ export function fromRelativePosition (y, rpos) {
if (rpos[0] === 'endof') {
let id
if (rpos[3] === null) {
id = new ID(rpos[1], rpos[2])
id = ID.createID(rpos[1], rpos[2])
} else {
id = new RootID(rpos[3], rpos[4])
id = ID.createRootID(rpos[3], rpos[4])
}
let type = y.os.get(id)
while (type._redone !== null) {
@@ -89,7 +88,7 @@ export function fromRelativePosition (y, rpos) {
}
} else {
let offset = 0
let struct = y.os.findNodeWithUpperBound(new ID(rpos[0], rpos[1])).val
let struct = y.os.findNodeWithUpperBound(ID.createID(rpos[0], rpos[1])).val
const diff = rpos[1] - struct._id.clock
while (struct._redone !== null) {
struct = struct._redone
+1 -33
View File
@@ -1,18 +1,3 @@
import Delete from '../Struct/Delete.js'
import ItemJSON from '../Struct/ItemJSON.js'
import ItemString from '../Struct/ItemString.js'
import ItemFormat from '../Struct/ItemFormat.js'
import ItemEmbed from '../Struct/ItemEmbed.js'
import GC from '../Struct/GC.js'
import YArray from '../Types/YArray/YArray.js'
import YMap from '../Types/YMap/YMap.js'
import YText from '../Types/YText/YText.js'
import YXmlText from '../Types/YXml/YXmlText.js'
import YXmlHook from '../Types/YXml/YXmlHook.js'
import YXmlFragment from '../Types/YXml/YXmlFragment.js'
import YXmlElement from '../Types/YXml/YXmlElement.js'
const structs = new Map()
const references = new Map()
@@ -21,7 +6,7 @@ const references = new Map()
* reference on all clients!
*
* @param {Number} reference
* @param {class} structConstructor
* @param {Function} structConstructor
*
* @public
*/
@@ -43,20 +28,3 @@ export function getStruct (reference) {
export function getStructReference (typeConstructor) {
return references.get(typeConstructor)
}
// TODO: reorder (Item* should have low numbers)
registerStruct(0, ItemJSON)
registerStruct(1, ItemString)
registerStruct(10, ItemFormat)
registerStruct(11, ItemEmbed)
registerStruct(2, Delete)
registerStruct(3, YArray)
registerStruct(4, YMap)
registerStruct(5, YText)
registerStruct(6, YXmlFragment)
registerStruct(7, YXmlElement)
registerStruct(8, YXmlText)
registerStruct(9, YXmlHook)
registerStruct(12, GC)
-54
View File
@@ -1,54 +0,0 @@
export { default as Y } from './Y.js'
export { default as UndoManager } from './Util/UndoManager.js'
export { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.js'
import Connector from './Connector.js'
import Persistence from './Persistence.js'
import YArray from './Types/YArray/YArray.js'
import YMap from './Types/YMap/YMap.js'
import YText from './Types/YText/YText.js'
import YXmlText from './Types/YXml/YXmlText.js'
import YXmlHook from './Types/YXml/YXmlHook.js'
import YXmlFragment from './Types/YXml/YXmlFragment.js'
import YXmlElement from './Types/YXml/YXmlElement.js'
import BinaryDecoder from './Util/Binary/Decoder.js'
import { getRelativePosition, fromRelativePosition } from './Util/relativePosition.js'
import { registerStruct } from './Util/structReferences.js'
import TextareaBinding from './Bindings/TextareaBinding/TextareaBinding.js'
import QuillBinding from './Bindings/QuillBinding/QuillBinding.js'
import DomBinding from './Bindings/DomBinding/DomBinding.js'
import { toBinary, fromBinary } from './MessageHandler/binaryEncode.js'
import domToType from './Bindings/DomBinding/domToType.js'
import { domsToTypes, switchAssociation } from './Bindings/DomBinding/util.js'
// TODO: The following assignments should be moved to yjs-dist
Y.AbstractConnector = Connector
Y.AbstractPersistence = Persistence
Y.Array = YArray
Y.Map = YMap
Y.Text = YText
Y.XmlElement = YXmlElement
Y.XmlFragment = YXmlFragment
Y.XmlText = YXmlText
Y.XmlHook = YXmlHook
Y.TextareaBinding = TextareaBinding
Y.QuillBinding = QuillBinding
Y.DomBinding = DomBinding
DomBinding.domToType = domToType
DomBinding.domsToTypes = domsToTypes
DomBinding.switchAssociation = switchAssociation
Y.utils = {
BinaryDecoder,
UndoManager,
getRelativePosition,
fromRelativePosition,
registerStruct,
integrateRemoteStructs,
toBinary,
fromBinary
}
+49 -108
View File
@@ -2,11 +2,17 @@ import DeleteStore from './Store/DeleteStore.js'
import OperationStore from './Store/OperationStore.js'
import StateStore from './Store/StateStore.js'
import { generateRandomUint32 } from './Util/generateRandomUint32.js'
import RootID from './Util/ID/RootID.js'
import { createRootID } from './Util/ID.js'
import NamedEventHandler from '../lib/NamedEventHandler.js'
import Transaction from './Util/Transaction.js'
import * as encoding from '../lib/encoding.js'
import * as message from './message.js'
import { integrateRemoteStructs } from './Util/integrateRemoteStructs.js'
export { default as DomBinding } from './Bindings/DomBinding/DomBinding.js'
/**
* @typedef {import('./Struct/Type.js').default} YType
* @typedef {import('../lib/decoding.js').Decoder} Decoder
*/
/**
* Anything that can be encoded with `JSON.stringify` and can be decoded with
@@ -17,18 +23,17 @@ export { default as DomBinding } from './Bindings/DomBinding/DomBinding.js'
*
* At the moment the only safe values are number and string.
*
* @typedef {(number|string)} encodable
* @typedef {(number|string|Object)} encodable
*/
/**
* A Yjs instance handles the state of shared data.
*
* @param {string} room Users in the same room share the same content
* @param {Object} opts Connector definition
* @param {AbstractPersistence} persistence Persistence adapter instance
* @param {Object} conf configuration
*/
export default class Y extends NamedEventHandler {
constructor (room, connector, persistence, conf = {}) {
constructor (room, conf = {}) {
super()
this.gcEnabled = conf.gc || false
/**
@@ -39,60 +44,46 @@ export default class Y extends NamedEventHandler {
this._contentReady = false
this.userID = generateRandomUint32()
// TODO: This should be a Map so we can use encodables as keys
this.share = {}
this.ds = new DeleteStore(this)
this._map = new Map()
this.ds = new DeleteStore()
this.os = new OperationStore(this)
this.ss = new StateStore(this)
this._missingStructs = new Map()
this._readyToIntegrate = []
this._transaction = null
/**
* The {@link AbstractConnector}.that is used by this Yjs instance.
* @type {AbstractConnector}
*/
this.connector = null
this.connected = false
let initConnection = () => {
if (connector != null) {
if (connector.constructor === Object) {
connector.connector.room = room
this.connector = new Y[connector.connector.name](this, connector.connector)
this.connected = true
this.emit('connectorReady')
}
}
}
/**
* The {@link AbstractPersistence} that is used by this Yjs instance.
* @type {AbstractPersistence}
*/
this.persistence = null
if (persistence != null) {
this.persistence = persistence
persistence._init(this).then(initConnection)
} else {
initConnection()
}
// for compatibility with isParentOf
this._parent = null
this._hasUndoManager = false
this._deleted = false // for compatiblity of having this as a parent for types
this._id = null
}
_setContentReady () {
if (!this._contentReady) {
this._contentReady = true
this.emit('content')
}
/**
* Read the Decoder and fill the Yjs instance with data in the decoder.
*
* @param {Decoder} decoder The BinaryDecoder to read from.
*/
importModel (decoder) {
this.transact(function () {
integrateRemoteStructs(decoder, this)
message.readDeleteSet(decoder, this)
})
}
whenContentReady () {
if (this._contentReady) {
return Promise.resolve()
} else {
return new Promise(resolve => {
this.once('content', resolve)
})
}
/**
* Encode the Yjs model to ArrayBuffer
*
* @return {ArrayBuffer} The Yjs model as ArrayBuffer
*/
exportModel () {
const encoder = encoding.createEncoder()
message.writeStructs(encoder, this, new Map())
message.writeDeleteSet(encoder, this)
return encoding.toBuffer(encoder)
}
_beforeChange () {}
_callObserver (transaction, subs, remote) {}
/**
* Changes that happen inside of a transaction are bundled. This means that
* the observer fires _after_ the transaction is finished and that all changes
@@ -157,9 +148,7 @@ export default class Y extends NamedEventHandler {
* @private
* Fake _start for root properties (y.set('name', type))
*/
set _start (start) {
return null
}
set _start (start) {}
/**
* Define a shared data type.
@@ -168,7 +157,7 @@ export default class Y extends NamedEventHandler {
* and do not overwrite each other. I.e.
* `y.define(name, type) === y.define(name, type)`
*
* After this method is called, the type is also available on `y.share[name]`.
* After this method is called, the type is also available on `y._map.get(name)`.
*
* *Best Practices:*
* Either define all types right after the Yjs instance is created or always
@@ -179,7 +168,7 @@ export default class Y extends NamedEventHandler {
* const y = new Y(..)
* y.define('myArray', YArray)
* y.define('myMap', YMap)
* // .. when accessing the type use y.share[name]
* // .. when accessing the type use y._map.get(name)
* y.share.myArray.insert(..)
* y.share.myMap.set(..)
*
@@ -190,15 +179,15 @@ export default class Y extends NamedEventHandler {
* y.define('myMap', YMap).set(..)
*
* @param {String} name
* @param {YType Constructor} TypeConstructor The constructor of the type definition
* @returns {YType} The created type
* @param {Function} TypeConstructor The constructor of the type definition
* @returns {YType} The created type. Constructed with TypeConstructor
*/
define (name, TypeConstructor) {
let id = new RootID(name, TypeConstructor)
let id = createRootID(name, TypeConstructor)
let type = this.os.get(id)
if (this.share[name] === undefined) {
this.share[name] = type
} else if (this.share[name] !== type) {
if (this._map.get(name) === undefined) {
this._map.set(name, type)
} else if (this._map.get(name) !== type) {
throw new Error('Type is already defined with a different constructor')
}
return type
@@ -213,66 +202,18 @@ export default class Y extends NamedEventHandler {
* @param {String} name The typename
*/
get (name) {
return this.share[name]
}
/**
* Disconnect this Yjs Instance from the network. The connector will
* unsubscribe from the room and document updates are not shared anymore.
*/
disconnect () {
if (this.connected) {
this.connected = false
return this.connector.disconnect()
} else {
return Promise.resolve()
}
}
/**
* If disconnected, tell the connector to reconnect to the room.
*/
reconnect () {
if (!this.connected) {
this.connected = true
return this.connector.reconnect()
} else {
return Promise.resolve()
}
return this._map.get(name)
}
/**
* Disconnect from the room, and destroy all traces of this Yjs instance.
* Persisted data will remain until removed by the persistence adapter.
*/
destroy () {
this.emit('destroyed', true)
super.destroy()
this.share = null
if (this.connector != null) {
if (this.connector.destroy != null) {
this.connector.destroy()
} else {
this.connector.disconnect()
}
}
if (this.persistence !== null) {
this.persistence.deinit(this)
this.persistence = null
}
this._map = null
this.os = null
this.ds = null
this.ss = null
}
}
Y.extend = function extendYjs () {
for (var i = 0; i < arguments.length; i++) {
var f = arguments[i]
if (typeof f === 'function') {
f(Y)
} else {
throw new Error('Expected a function!')
}
}
}
+10 -10
View File
@@ -1,15 +1,15 @@
/* eslint-env browser */
import * as idbactions from './idbactions.js'
import * as globals from './globals.js'
import * as globals from '../../lib/globals.js'
import * as message from './message.js'
import * as bc from './broadcastchannel.js'
import * as encoding from './encoding.js'
import * as logging from './logging.js'
import * as idb from './idb.js'
import Y from '../src/Y.js'
import BinaryDecoder from '../src/Util/Binary/Decoder.js'
import { integrateRemoteStruct } from '../src/MessageHandler/integrateRemoteStructs.js'
import { createMutualExclude } from '../src/Util/mutualExclude.js'
import * as encoding from '../../lib/encoding.js'
import * as logging from '../../lib/logging.js'
import * as idb from '../../lib/idb.js'
import * as decoding from '../../lib/decoding.js'
import Y from '../Y.js'
import { integrateRemoteStruct } from '../MessageHandler/integrateRemoteStructs.js'
import { createMutualExclude } from '../../lib/mutualExclude.js'
import * as NamedEventHandler from './NamedEventHandler.js'
@@ -70,8 +70,8 @@ export class YdbClient extends NamedEventHandler.Class {
}))
subscribe(this, roomname, update => mutex(() => {
y.transact(() => {
const decoder = new BinaryDecoder(update)
while (decoder.hasContent()) {
const decoder = decoding.createDecoder(update)
while (decoding.hasContent(decoder)) {
integrateRemoteStruct(y, decoder)
}
}, true)
+4 -4
View File
@@ -29,10 +29,10 @@
* - A client may update a room when the room is in either US or Co
*/
import * as encoding from './encoding.js'
import * as decoding from './decoding.js'
import * as idb from './idb.js'
import * as globals from './globals.js'
import * as encoding from '../../lib/encoding.js'
import * as decoding from '../../lib/decoding.js'
import * as idb from '../../lib/idb.js'
import * as globals from '../../lib/globals.js'
import * as message from './message.js'
/**
+55
View File
@@ -0,0 +1,55 @@
import Delete from './Struct/Delete.js'
import ItemJSON from './Struct/ItemJSON.js'
import ItemString from './Struct/ItemString.js'
import ItemFormat from './Struct/ItemFormat.js'
import ItemEmbed from './Struct/ItemEmbed.js'
import GC from './Struct/GC.js'
import YArray from './Types/YArray/YArray.js'
import YMap from './Types/YMap/YMap.js'
import YText from './Types/YText/YText.js'
import YXmlText from './Types/YXml/YXmlText.js'
import YXmlHook from './Types/YXml/YXmlHook.js'
import YXmlFragment from './Types/YXml/YXmlFragment.js'
import YXmlElement from './Types/YXml/YXmlElement.js'
import { registerStruct } from './Util/structReferences.js'
export { default as Y } from './Y.js'
export { default as UndoManager } from './Util/UndoManager.js'
export { default as Transaction } from './Util/Transaction.js'
export { default as Array } from './Types/YArray/YArray.js'
export { default as Map } from './Types/YMap/YMap.js'
export { default as Text } from './Types/YText/YText.js'
export { default as XmlText } from './Types/YXml/YXmlText.js'
export { default as XmlHook } from './Types/YXml/YXmlHook.js'
export { default as XmlFragment } from './Types/YXml/YXmlFragment.js'
export { default as XmlElement } from './Types/YXml/YXmlElement.js'
export { getRelativePosition, fromRelativePosition } from './Util/relativePosition.js'
export { registerStruct as registerType } from './Util/structReferences.js'
export { default as TextareaBinding } from './Bindings/TextareaBinding/TextareaBinding.js'
export { default as QuillBinding } from './Bindings/QuillBinding/QuillBinding.js'
export { default as DomBinding } from './Bindings/DomBinding/DomBinding.js'
export { default as domToType } from './Bindings/DomBinding/domToType.js'
export { domsToTypes, switchAssociation } from './Bindings/DomBinding/util.js'
// TODO: reorder (Item* should have low numbers)
registerStruct(0, ItemJSON)
registerStruct(1, ItemString)
registerStruct(10, ItemFormat)
registerStruct(11, ItemEmbed)
registerStruct(2, Delete)
registerStruct(3, YArray)
registerStruct(4, YMap)
registerStruct(5, YText)
registerStruct(6, YXmlFragment)
registerStruct(7, YXmlElement)
registerStruct(8, YXmlText)
registerStruct(9, YXmlHook)
registerStruct(12, GC)
+487
View File
@@ -0,0 +1,487 @@
import * as encoding from '../lib/encoding.js'
import * as decoding from '../lib/decoding.js'
import * as ID from './Util/ID.js'
import { getStruct } from './Util/structReferences.js'
import { deleteItemRange } from './Struct/Delete.js'
import { integrateRemoteStruct } from './Util/integrateRemoteStructs.js'
import Item from './Struct/Item.js'
/**
* @typedef {import('./Store/StateStore.js').default} StateStore
* @typedef {import('./Y.js').default} Y
* @typedef {import('./Struct/Item.js').default} Item
* @typedef {import('./Store/StateStore.js').StateSet} StateSet
*/
/**
* Core Yjs only defines three message types:
* • YjsSyncStep1: Includes the State Set of the sending client. When received, the client should reply with YjsSyncStep2.
* • YjsSyncStep2: Includes all missing structs and the complete delete set. When received, the the client is assured that
* it received all information from the remote client.
*
* In a peer-to-peer network, you may want to introduce a SyncDone message type. Both parties should initiate the connection
* with SyncStep1. When a client received SyncStep2, it should reply with SyncDone. When the local client received both
* SyncStep2 and SyncDone, it is assured that it is synced to the remote client.
*
* In a client-server model, you want to handle this differently: The client should initiate the connection with SyncStep1.
* When the server receives SyncStep1, it should reply with SyncStep2 immediately followed by SyncStep1. The client replies
* with SyncStep2 when it receives SyncStep1. Optionally the server may send a SyncDone after it received SyncStep2, so the
* client knows that the sync is finished. There are two reasons for this more elaborated sync model: 1. This protocol can
* easily be implemented on top of http and websockets. 2. The server shoul only reply to requests, and not initiate them.
* Therefore it is necesarry that the client initiates the sync.
*
* Construction of a message:
* [messageType : varUint, message definition..]
*
* Note: A message does not include information about the room name. This must to be handled by the upper layer protocol!
*
* stringify[messageType] stringifies a message definition (messageType is already read from the bufffer)
*/
const messageYjsSyncStep1 = 0
const messageYjsSyncStep2 = 1
const messageYjsUpdate = 2
/**
* Stringifies a message-encoded Delete Set.
*
* @param {decoding.Decoder} decoder
* @return {string}
*/
export const stringifyDeleteSet = (decoder) => {
let str = ''
const dsLength = decoding.readUint32(decoder)
for (let i = 0; i < dsLength; i++) {
str += ' -' + decoding.readVarUint(decoder) + ':\n' // decodes user
const dvLength = decoding.readVarUint(decoder)
for (let j = 0; j < dvLength; j++) {
str += `clock: ${decoding.readVarUint(decoder)}, length: ${decoding.readVarUint(decoder)}, gc: ${decoding.readUint8(decoder) === 1}\n`
}
}
return str
}
/**
* Write the DeleteSet of a shared document to an Encoder.
*
* @param {encoding.Encoder} encoder
* @param {Y} y
*/
export const writeDeleteSet = (encoder, y) => {
let currentUser = null
let currentLength
let lastLenPos
let numberOfUsers = 0
const laterDSLenPus = encoding.length(encoder)
encoding.writeUint32(encoder, 0)
y.ds.iterate(null, null, n => {
const user = n._id.user
const clock = n._id.clock
const len = n.len
const gc = n.gc
if (currentUser !== user) {
numberOfUsers++
// a new user was found
if (currentUser !== null) { // happens on first iteration
encoding.setUint32(encoder, lastLenPos, currentLength)
}
currentUser = user
encoding.writeVarUint(encoder, user)
// pseudo-fill pos
lastLenPos = encoding.length(encoder)
encoding.writeUint32(encoder, 0)
currentLength = 0
}
encoding.writeVarUint(encoder, clock)
encoding.writeVarUint(encoder, len)
encoding.writeUint8(encoder, gc ? 1 : 0)
currentLength++
})
if (currentUser !== null) { // happens on first iteration
encoding.setUint32(encoder, lastLenPos, currentLength)
}
encoding.setUint32(encoder, laterDSLenPus, numberOfUsers)
}
/**
* Read delete set from Decoder and apply it to a shared document.
*
* @param {decoding.Decoder} decoder
* @param {Y} y
*/
export const readDeleteSet = (decoder, y) => {
const dsLength = decoding.readUint32(decoder)
for (let i = 0; i < dsLength; i++) {
const user = decoding.readVarUint(decoder)
const dv = []
const dvLength = decoding.readUint32(decoder)
for (let j = 0; j < dvLength; j++) {
const from = decoding.readVarUint(decoder)
const len = decoding.readVarUint(decoder)
const gc = decoding.readUint8(decoder) === 1
dv.push({from, len, gc})
}
if (dvLength > 0) {
const deletions = []
let pos = 0
let d = dv[pos]
y.ds.iterate(ID.createID(user, 0), ID.createID(user, Number.MAX_VALUE), n => {
// cases:
// 1. d deletes something to the right of n
// => go to next n (break)
// 2. d deletes something to the left of n
// => create deletions
// => reset d accordingly
// *)=> if d doesn't delete anything anymore, go to next d (continue)
// 3. not 2) and d deletes something that also n deletes
// => reset d so that it doesn't contain n's deletion
// *)=> if d does not delete anything anymore, go to next d (continue)
while (d != null) {
var diff = 0 // describe the diff of length in 1) and 2)
if (n._id.clock + n.len <= d.from) {
// 1)
break
} else if (d.from < n._id.clock) {
// 2)
// delete maximum the len of d
// else delete as much as possible
diff = Math.min(n._id.clock - d.from, d.len)
// deleteItemRange(y, user, d.from, diff, true)
deletions.push([user, d.from, diff])
} else {
// 3)
diff = n._id.clock + n.len - d.from // never null (see 1)
if (d.gc && !n.gc) {
// d marks as gc'd but n does not
// then delete either way
// deleteItemRange(y, user, d.from, Math.min(diff, d.len), true)
deletions.push([user, d.from, Math.min(diff, d.len)])
}
}
if (d.len <= diff) {
// d doesn't delete anything anymore
d = dv[++pos]
} else {
d.from = d.from + diff // reset pos
d.len = d.len - diff // reset length
}
}
})
// TODO: It would be more performant to apply the deletes in the above loop
// Adapt the Tree implementation to support delete while iterating
for (let i = deletions.length - 1; i >= 0; i--) {
const del = deletions[i]
deleteItemRange(y, del[0], del[1], del[2], true)
}
// for the rest.. just apply it
for (; pos < dv.length; pos++) {
d = dv[pos]
deleteItemRange(y, user, d.from, d.len, true)
// deletions.push([user, d.from, d.len, d.gc)
}
}
}
}
/**
* Read a StateSet from Decoder and return it as string.
*
* @param {decoding.Decoder} decoder
* @return {string}
*/
export const stringifyStateSet = decoder => {
let s = 'State Set: '
readStateSet(decoder).forEach((user, userState) => {
s += `(${user}: ${userState}), `
})
return s
}
/**
* Write StateSet to Encoder
*
* @param {Y} y
* @param {encoding.Encoder} encoder
*/
export const writeStateSet = (encoder, y) => {
const state = y.ss.state
// write as fixed-size number to stay consistent with the other encode functions.
// => anytime we write the number of objects that follow, encode as fixed-size number.
encoding.writeUint32(encoder, state.size)
state.forEach((user, clock) => {
encoding.writeVarUint(encoder, user)
encoding.writeVarUint(encoder, clock)
})
}
/**
* Read StateSet from Decoder and return as Map
*
* @param {decoding.Decoder} decoder
* @return {StateSet}
*/
export const readStateSet = decoder => {
const ss = new Map()
const ssLength = decoding.readUint32(decoder)
for (let i = 0; i < ssLength; i++) {
const user = decoding.readVarUint(decoder)
const clock = decoding.readVarUint(decoder)
ss.set(user, clock)
}
return ss
}
/**
* Stringify an item id.
*
* @param {ID.ID | ID.RootID} id
* @return {string}
*/
export const stringifyID = id => id instanceof ID.ID ? `(${id.user},${id.clock})` : `(${id.name},${id.type})`
/**
* Stringify an item as ID. HHere, an item could also be a Yjs instance (e.g. item._parent).
*
* @param {Item | Y | null} item
* @return {string}
*/
export const stringifyItemID = item => {
let result
if (item === null) {
result = '()'
} else if (item instanceof Item) {
result = stringifyID(item._id)
} else {
// must be a Yjs instance
// Don't include Y in this module, so we prevent circular dependencies.
result = 'y'
}
return result
}
/**
* Helper utility to convert an item to a readable format.
*
* @param {String} name The name of the item class (YText, ItemString, ..).
* @param {Item} item The item instance.
* @param {String} [append] Additional information to append to the returned
* string.
* @return {String} A readable string that represents the item object.
*
*/
export const logItemHelper = (name, item, append) => {
const left = item._left !== null ? stringifyID(item._left._lastId) : '()'
const origin = item._origin !== null ? stringifyID(item._origin._lastId) : '()'
return `${name}(id:${stringifyItemID(item)},left:${left},origin:${origin},right:${stringifyItemID(item._right)},parent:${stringifyItemID(item._parent)},parentSub:${item._parentSub}${append !== undefined ? ' - ' + append : ''})`
}
/**
* @param {decoding.Decoder} decoder
* @param {Y} y
* @return {string}
*/
export const stringifyStructs = (decoder, y) => {
let str = ''
const len = decoding.readUint32(decoder)
for (let i = 0; i < len; i++) {
let reference = decoding.readVarUint(decoder)
let Constr = getStruct(reference)
let struct = new Constr()
let missing = struct._fromBinary(y, decoder)
let logMessage = ' ' + struct._logString()
if (missing.length > 0) {
logMessage += ' .. missing: ' + missing.map(stringifyItemID).join(', ')
}
str += logMessage + '\n'
}
return str
}
/**
* Write all Items that are not not included in ss to
* the encoder object.
*
* @param {encoding.Encoder} encoder
* @param {Y} y
* @param {StateSet} ss State Set received from a remote client. Maps from client id to number of created operations by client id.
*/
export const writeStructs = (encoder, y, ss) => {
const lenPos = encoding.length(encoder)
encoding.writeUint32(encoder, 0)
let len = 0
for (let user of y.ss.state.keys()) {
let clock = ss.get(user) || 0
if (user !== ID.RootFakeUserID) {
const minBound = ID.createID(user, clock)
const overlappingLeft = y.os.findPrev(minBound)
const rightID = overlappingLeft === null ? null : overlappingLeft._id
if (rightID !== null && rightID.user === user && rightID.clock + overlappingLeft._length > clock) {
const struct = overlappingLeft._clonePartial(clock - rightID.clock)
struct._toBinary(encoder)
len++
}
y.os.iterate(minBound, ID.createID(user, Number.MAX_VALUE), struct => {
struct._toBinary(encoder)
len++
})
}
}
encoding.setUint32(encoder, lenPos, len)
}
/**
* Read structs and delete operations from decoder and apply them on a shared document.
*
* @param {decoding.Decoder} decoder
* @param {Y} y
*/
export const readStructs = (decoder, y) => {
const len = decoding.readUint32(decoder)
for (let i = 0; i < len; i++) {
integrateRemoteStruct(decoder, y)
}
}
/**
* Read SyncStep1 and return it as a readable string.
*
* @param {decoding.Decoder} decoder
* @return {string}
*/
export const stringifySyncStep1 = (decoder) => {
let s = 'SyncStep1: '
const len = decoding.readUint32(decoder)
for (let i = 0; i < len; i++) {
const user = decoding.readVarUint(decoder)
const clock = decoding.readVarUint(decoder)
s += `(${user}:${clock})`
}
return s
}
/**
* Create a sync step 1 message based on the state of the current shared document.
*
* @param {encoding.Encoder} encoder
* @param {Y} y
*/
export const writeSyncStep1 = (encoder, y) => {
encoding.writeVarUint(encoder, messageYjsSyncStep1)
writeStateSet(encoder, y)
}
/**
* Read SyncStep1 message and reply with SyncStep2.
*
* @param {decoding.Decoder} decoder The reply to the received message
* @param {encoding.Encoder} encoder The received message
* @param {Y} y
*/
export const readSyncStep1 = (decoder, encoder, y) => {
// read sync step 1 message
const ss = readStateSet(decoder)
// write sync step 2
encoding.writeVarUint(encoder, messageYjsSyncStep2)
writeStructs(encoder, y, ss)
writeDeleteSet(encoder, y)
}
/**
* @param {decoding.Decoder} decoder
* @param {Y} y
* @return {string}
*/
export const stringifySyncStep2 = (decoder, y) => {
let str = ' == Sync step 2:\n'
str += ' + Structs:\n'
str += stringifyStructs(decoder, y)
// write DS to string
str += ' + Delete Set:\n'
str += stringifyDeleteSet(decoder)
return str
}
/**
* Read and apply Structs and then DeleteSet to a y instance.
*
* @param {decoding.Decoder} decoder
* @param {encoding.Encoder} encoder
* @param {Y} y
*/
export const readSyncStep2 = (decoder, encoder, y) => {
readStructs(decoder, y)
readDeleteSet(decoder, y)
}
/**
* @param {decoding.Decoder} decoder
* @param {Y} y
* @return {string}
*/
export const stringifyUpdate = (decoder, y) =>
' == Update:\n' + stringifyStructs(decoder, y)
/**
* @param {encoding.Encoder} encoder
* @param {encoding.Encoder} updates
*/
export const writeUpdate = (encoder, numOfStructs, updates) => {
encoding.writeVarUint(encoder, messageYjsUpdate)
encoding.writeUint32(encoder, numOfStructs)
encoding.writeBinaryEncoder(encoder, updates)
}
export const readUpdate = readStructs
/**
* @param {decoding.Decoder} decoder
* @param {Y} y
* @return {string} The message converted to string
*/
export const stringifyMessage = (decoder, y) => {
const messageType = decoding.readVarUint(decoder)
let stringifiedMessage
let stringifiedMessageType
switch (messageType) {
case messageYjsSyncStep1:
stringifiedMessageType = 'YjsSyncStep1'
stringifiedMessage = stringifySyncStep1(decoder)
break
case messageYjsSyncStep2:
stringifiedMessageType = 'YjsSyncStep2'
stringifiedMessage = stringifySyncStep2(decoder, y)
break
case messageYjsUpdate:
stringifiedMessageType = 'YjsUpdate'
stringifiedMessage = stringifyStructs(decoder, y)
break
default:
stringifiedMessageType = 'Unknown'
stringifiedMessage = 'Unknown'
}
return `Message ${stringifiedMessageType}:\n${stringifiedMessage}`
}
/**
* @param {decoding.Decoder} decoder A message received from another client
* @param {encoding.Encoder} encoder The reply message. Will not be sent if empty.
* @param {Y} y
*/
export const readMessage = (decoder, encoder, y) => {
const messageType = decoding.readVarUint(decoder)
switch (messageType) {
case messageYjsSyncStep1:
readSyncStep1(decoder, encoder, y)
break
case messageYjsSyncStep2:
y.transact(() => readSyncStep2(decoder, encoder, y), true)
break
case messageYjsUpdate:
y.transact(() => readUpdate(decoder, y), true)
break
default:
throw new Error('Unknown message type')
}
return messageType
}