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

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).

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

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

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).
*

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)

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

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) {

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
}

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]])
}
}
}
}

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 : ''})`
}

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)
}

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)
}
}

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)
}

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()

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
*/

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
}

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

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()

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)
}

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}`
}
}

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
}

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
}

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.

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.

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)
}
}
/**

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.

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) {

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)}"`)
}
}

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.
*

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.
*

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

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
*

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

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)
}
/**

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
*/

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
src/Util/ID.js Normal file
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))
}

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
}
}
}

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
}
}
}

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()
}
}

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)

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.

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) {

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)

View File

@@ -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

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

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

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)

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
}

157
src/Y.js
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!')
}
}
}

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)

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
src/index.js Normal file
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
src/message.js Normal file
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
}