diff --git a/src/Bindings/Binding.js b/src/Bindings/Binding.js index 8ccab2c2..195550b0 100644 --- a/src/Bindings/Binding.js +++ b/src/Bindings/Binding.js @@ -2,7 +2,7 @@ import { createMutualExclude } from '../Util/mutualExclude.js' /** - * Abstract class for bindings + * Abstract class for bindings. * * A binding handles data binding from a Yjs type to a data object. For example, * you can bind a Quill editor instance to a YText instance with the `QuillBinding` class. @@ -18,16 +18,27 @@ import { createMutualExclude } from '../Util/mutualExclude.js' */ export default class Binding { /** - * @param {YType} type Yjs type - * @param {any} target Binding Target + * @param {YType} type Yjs type. + * @param {any} target Binding Target. */ constructor (type, target) { + /** + * The Yjs type that is bound to `target` + * @type {YType} + */ this.type = type + /** + * The target that `type` is bound to. + * @type {*} + */ this.target = target + /** + * @private + */ this._mutualExclude = createMutualExclude() } /** - * Remove all data observers (both from the type and th target). + * Remove all data observers (both from the type and the target). */ destroy () { this.type = null diff --git a/src/Bindings/DomBinding/DomBinding.js b/src/Bindings/DomBinding/DomBinding.js index d8b06dab..0694b66a 100644 --- a/src/Bindings/DomBinding/DomBinding.js +++ b/src/Bindings/DomBinding/DomBinding.js @@ -17,9 +17,9 @@ import { removeAssociation } from './util.js' * This binding is automatically destroyed when its parent is deleted. * * @example - * const div = document.createElement('div') - * const type = y.define('xml', Y.XmlFragment) - * const binding = new Y.QuillBinding(type, div) + * const div = document.createElement('div') + * const type = y.define('xml', Y.XmlFragment) + * const binding = new Y.QuillBinding(type, div) * */ export default class DomBinding extends Binding { @@ -27,12 +27,28 @@ export default class DomBinding extends Binding { * @param {YXmlFragment} type The bind source. This is the ultimate source of * truth. * @param {Element} target The bind target. Mirrors the target. + * @param {Object} [opts] Optional configurations + + * @param {FilterFunction} [opts.filter=defaultFilter] The filter function to use. */ constructor (type, target, opts = {}) { // Binding handles textType as this.type and domTextarea as this.target super(type, target) + /** + * Maps each DOM element to the type that it is associated with. + * @type {Map} + */ this.domToType = new Map() + /** + * Maps each YXml type to the DOM element that it is associated with. + * @type {Map} + */ this.typeToDom = new Map() + /** + * Defines which DOM attributes and elements to filter out. + * Also filters remote changes. + * @type {FilterFunction} + */ this.filter = opts.filter || defaultFilter // set initial value target.innerHTML = '' @@ -103,6 +119,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. */ setFilter (filter) { this.filter = filter @@ -110,7 +127,7 @@ export default class DomBinding extends Binding { } /** - * Remove all properties that are handled by this class + * Remove all properties that are handled by this class. */ destroy () { this.domToType = null @@ -125,3 +142,11 @@ export default class DomBinding extends Binding { super.destroy() } } + + /** + * A filter defines which elements and attributes to share. + * Return null if the node should be filtered. Otherwise return the Map of + * accepted attributes. + * + * @typedef {function(nodeName: String, attrs: Map): Map|null} FilterFunction + */ diff --git a/src/Bindings/DomBinding/domObserver.js b/src/Bindings/DomBinding/domObserver.js index f0f4d6bb..8304c079 100644 --- a/src/Bindings/DomBinding/domObserver.js +++ b/src/Bindings/DomBinding/domObserver.js @@ -7,7 +7,7 @@ import { import diff from '../../Util/simpleDiff.js' import YXmlFragment from '../../Types/YXml/YXmlFragment.js' -/* +/** * 1. Check if any of the nodes was deleted * 2. Iterate over the children. * 2.1 If a node exists that is not yet bound to a type, insert a new node @@ -17,6 +17,7 @@ import YXmlFragment from '../../Types/YXml/YXmlFragment.js' * recreate a new yxml element that is bound to that node. * You can detect that a node was moved because expectedId * !== actualId in the list + * @private */ function applyChangesFromDom (binding, dom, yxml, _document) { if (yxml == null || yxml === false || yxml.constructor === YXmlHook) { @@ -79,6 +80,9 @@ function applyChangesFromDom (binding, dom, yxml, _document) { } } +/** + * @private + */ export default function domObserver (mutations, _document) { this._mutualExclude(() => { this.type._y.transact(() => { diff --git a/src/Bindings/DomBinding/domToType.js b/src/Bindings/DomBinding/domToType.js index ddf21a6d..d95f8d28 100644 --- a/src/Bindings/DomBinding/domToType.js +++ b/src/Bindings/DomBinding/domToType.js @@ -5,7 +5,11 @@ import { createAssociation } from './util.js' /** * Creates a Yjs type (YXml) based on the contents of a DOM Element. * - * @param {Element|TextNode} + * @param {Element|TextNode} element The DOM Element + * @param {?Document} _document Optional. Provide the global document object. + * @param {?DomBinding} binding This property should only be set if the type + * is going to be bound with the dom-binding. + * @return {YXmlElement | YXmlText} */ export default function domToType (element, _document = document, binding) { let type diff --git a/src/Bindings/DomBinding/filter.js b/src/Bindings/DomBinding/filter.js index e9cb8b87..81fac728 100644 --- a/src/Bindings/DomBinding/filter.js +++ b/src/Bindings/DomBinding/filter.js @@ -1,10 +1,27 @@ import isParentOf from '../../Util/isParentOf.js' +/** + * Default filter method (does nothing). + * + * @param {String} nodeName The nodeName of the element + * @param {Map} attrs Map of key-value pairs that are attributes of the node. + * @return {Map | null} The allowed attributes or null, if the element should be + * filtered. + */ export function defaultFilter (nodeName, attrs) { + // TODO: implement basic filter that filters out dangerous properties! return attrs } - +/** + * Applies a filter on a type. + * + * @param {Y} y The Yjs instance. + * @param {DomBinding} binding The DOM binding instance that has the dom filter. + * @param {YXmlElement | YXmlFragment } type The type to apply the filter to. + * + * @private + */ export function applyFilterOnType (y, binding, type) { if (isParentOf(binding.type, type)) { const nodeName = type.nodeName diff --git a/src/Bindings/DomBinding/selection.js b/src/Bindings/DomBinding/selection.js index 551f4e87..366a1db9 100644 --- a/src/Bindings/DomBinding/selection.js +++ b/src/Bindings/DomBinding/selection.js @@ -5,6 +5,9 @@ import { getRelativePosition, fromRelativePosition } from '../../Util/relativePo let browserSelection = null let relativeSelection = null +/** + * @private + */ export let beforeTransactionSelectionFixer if (typeof getSelection !== 'undefined') { beforeTransactionSelectionFixer = function _beforeTransactionSelectionFixer (y, domBinding, transaction, remote) { @@ -30,6 +33,9 @@ if (typeof getSelection !== 'undefined') { beforeTransactionSelectionFixer = function _fakeBeforeTransactionSelectionFixer () {} } +/** + * @private + */ export function afterTransactionSelectionFixer (y, domBinding, transaction, remote) { if (relativeSelection === null || !remote) { return diff --git a/src/Bindings/DomBinding/typeObserver.js b/src/Bindings/DomBinding/typeObserver.js index 46f8c79d..e5b4343a 100644 --- a/src/Bindings/DomBinding/typeObserver.js +++ b/src/Bindings/DomBinding/typeObserver.js @@ -3,6 +3,9 @@ import YXmlText from '../../Types/YXml/YXmlText.js' import YXmlHook from '../../Types/YXml/YXmlHook.js' import { removeDomChildrenUntilElementFound } from './util.js' +/** + * @private + */ export default function typeObserver (events, _document) { this._mutualExclude(() => { events.forEach(event => { diff --git a/src/Bindings/DomBinding/util.js b/src/Bindings/DomBinding/util.js index f8e5c94c..5396766b 100644 --- a/src/Bindings/DomBinding/util.js +++ b/src/Bindings/DomBinding/util.js @@ -1,6 +1,11 @@ import domToType from './domToType.js' +/** + * Iterates items until an undeleted item is found. + * + * @private + */ export function iterateUntilUndeleted (item) { while (item !== null && item._deleted) { item = item._right @@ -8,11 +13,23 @@ export function iterateUntilUndeleted (item) { return item } +/** + * Removes an association (the information that a DOM element belongs to a + * type). + * + * @private + */ export function removeAssociation (domBinding, dom, type) { domBinding.domToType.delete(dom) domBinding.typeToDom.delete(type) } +/** + * Creates an association (the information that a DOM element belongs to a + * type). + * + * @private + */ export function createAssociation (domBinding, dom, type) { if (domBinding !== undefined) { domBinding.domToType.set(dom, type) @@ -31,12 +48,18 @@ export function createAssociation (domBinding, dom, type) { * the beginning. * @param {Array} doms The Dom elements to insert. * @param {?Document} _document Optional. Provide the global document object. + * @param {DomBinding} binding The dom binding * @return {Array} The YxmlElements that are inserted. + * + * @private */ export function insertDomElementsAfter (type, prev, doms, _document, binding) { return type.insertAfter(prev, doms.map(dom => domToType(dom, _document, binding))) } +/** + * @private + */ export function insertNodeHelper (yxml, prevExpectedNode, child, _document, binding) { let insertedNodes = insertDomElementsAfter(yxml, prevExpectedNode, [child], _document, binding) if (insertedNodes.length > 0) { @@ -54,6 +77,8 @@ export function insertNodeHelper (yxml, prevExpectedNode, child, _document, bind * @param {Element} currentChild Start removing elements with `currentChild`. If * `currentChild` is `elem` it won't be removed. * @param {Element|null} elem The elemnt to look for. + * + * @private */ export function removeDomChildrenUntilElementFound (parent, currentChild, elem) { while (currentChild !== elem) { diff --git a/src/Connector.js b/src/Connector.js index a76fd687..654a2e9f 100644 --- a/src/Connector.js +++ b/src/Connector.js @@ -7,6 +7,8 @@ import { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs. import debug from 'debug' +// TODO: rename Connector + export default class AbstractConnector { constructor (y, opts) { this.y = y diff --git a/src/Persistence.js b/src/Persistence.js index 851e868a..20fc9178 100644 --- a/src/Persistence.js +++ b/src/Persistence.js @@ -14,7 +14,6 @@ function getFreshCnf () { } /** - * @private * Abstract persistence class. */ export default class AbstractPersistence { diff --git a/src/Transaction.js b/src/Transaction.js index 4b372fdf..237ed166 100644 --- a/src/Transaction.js +++ b/src/Transaction.js @@ -1,26 +1,69 @@ /** - * Changes that are created within a transaction are bundled and sent as one - * message to the remote peers. This implies that the changes are applied - * in one flush and at most one {@link YEvent} per type is created. + * A transaction is created for every change on the Yjs model. It is possible + * to bundle changes on the Yjs model in a single transaction to + * minimize the number on messages sent and the number of observer calls. + * If possible the user of this library should bundle as many changes as + * possible. Here is an example to illustrate the advantages of bundling: + * + * @example + * const map = y.define('map', YMap) + * // Log content when change is triggered + * map.observe(function () { + * console.log('change triggered') + * }) + * // Each change on the map type triggers a log message: + * map.set('a', 0) // => "change triggered" + * map.set('b', 0) // => "change triggered" + * // When put in a transaction, it will trigger the log after the transaction: + * y.transact(function () { + * map.set('a', 1) + * map.set('b', 1) + * }) // => "change triggered" * - * It is best to bundle as many changes in a single Transaction as possible. - * This way only few changes need to be computed */ export default class Transaction { constructor (y) { + /** + * @type {Y} The Yjs instance. + */ this.y = y - // types added during transaction + /** + * All new types that are added during a transaction. + * @type {Set} + */ this.newTypes = new Set() - // changed types (does not include new types) - // maps from type to parentSubs (item._parentSub = null for array elements) + /** + * 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} + */ this.changedTypes = new Map() + // TODO: rename deletedTypes + /** + * Set of all deleted Types and Structs. + * @type {Set} + */ this.deletedStructs = new Set() + /** + * Saves the old state set of the Yjs instance. If a state was modified, + * the original value is saved here. + * @type {Map} + */ this.beforeState = new Map() + /** + * Stores the events for the types that observe also child elements. + * It is mainly used by `observeDeep`. + * @type {Map>} + */ this.changedParentTypes = new Map() } } +/** + * @private + */ export function transactionTypeChanged (y, type, sub) { if (type !== y && !type._deleted && !y._transaction.newTypes.has(type)) { const changedTypes = y._transaction.changedTypes diff --git a/src/Util/Binary/Encoder.js b/src/Util/Binary/Encoder.js index 42e561e4..53e09260 100644 --- a/src/Util/Binary/Encoder.js +++ b/src/Util/Binary/Encoder.js @@ -9,6 +9,7 @@ const bits8 = 0b11111111 export default class BinaryEncoder { constructor () { // TODO: implement chained Uint8Array buffers instead of Array buffer + // TODO: Rewrite all methods as functions! this.data = [] } diff --git a/src/Util/Tree.js b/src/Util/Tree.js index 7cca3767..d39060a3 100644 --- a/src/Util/Tree.js +++ b/src/Util/Tree.js @@ -467,5 +467,4 @@ export default class Tree { } } } - flush () {} } diff --git a/src/Util/UndoManager.js b/src/Util/UndoManager.js index a074ce7c..56cf2d95 100644 --- a/src/Util/UndoManager.js +++ b/src/Util/UndoManager.js @@ -1,4 +1,5 @@ import ID from './ID/ID.js' +import isParentOf from './isParentOf.js' class ReverseOperation { constructor (y, transaction) { @@ -15,16 +16,6 @@ class ReverseOperation { } } -function isStructInScope (y, struct, scope) { - while (struct !== y) { - if (struct === scope) { - return true - } - struct = struct._parent - } - return false -} - function applyReverseOperation (y, scope, reverseBuffer) { let performedUndo = false y.transact(() => { @@ -38,7 +29,7 @@ function applyReverseOperation (y, scope, reverseBuffer) { while (op._deleted && op._redone !== null) { op = op._redone } - if (op._deleted === false && isStructInScope(y, op, scope)) { + if (op._deleted === false && isParentOf(scope, op)) { performedUndo = true op._delete(y) } @@ -46,7 +37,7 @@ function applyReverseOperation (y, scope, reverseBuffer) { } for (let op of undoOp.deletedStructs) { if ( - isStructInScope(y, op, scope) && + isParentOf(scope, op) && op._parent !== y && ( op._id.user !== y.userID || diff --git a/src/Util/YEvent.js b/src/Util/YEvent.js index 1af22be4..b9096db9 100644 --- a/src/Util/YEvent.js +++ b/src/Util/YEvent.js @@ -7,7 +7,15 @@ export default class YEvent { * @param {YType} target The changed type. */ constructor (target) { + /** + * The type on which this event was created on. + * @type {YType} + */ this.target = target + /** + * The current target on which the observe callback is called. + * @type {YType} + */ this.currentTarget = target } diff --git a/src/Util/generateUserID.js b/src/Util/generateRandomUint32.js similarity index 90% rename from src/Util/generateUserID.js rename to src/Util/generateRandomUint32.js index d6f18701..23ac9b27 100644 --- a/src/Util/generateUserID.js +++ b/src/Util/generateRandomUint32.js @@ -1,6 +1,6 @@ /* global crypto */ -export function generateUserID () { +export function generateRandomUint32 () { if (typeof crypto !== 'undefined' && crypto.getRandomValue != null) { // browser let arr = new Uint32Array(1) diff --git a/src/Util/isParentOf.js b/src/Util/isParentOf.js index 76c3deab..f56b5f66 100644 --- a/src/Util/isParentOf.js +++ b/src/Util/isParentOf.js @@ -5,6 +5,8 @@ * @param {Type} parent * @param {Type} child * @return {Boolean} Whether `parent` is a parent of `child`. + * + * @public */ export default function isParentOf (parent, child) { child = child._parent diff --git a/src/Util/mutualExclude.js b/src/Util/mutualExclude.js index 519dfd50..9a83fa29 100644 --- a/src/Util/mutualExclude.js +++ b/src/Util/mutualExclude.js @@ -1,4 +1,22 @@ +// TODO: rename mutex + +/** + * Creates a mutual exclude function with the following property: + * + * @example + * const mutualExclude = createMutualExclude() + * mutualExclude(function () { + * // This function is immediately executed + * mutualExclude(function () { + * // This function is never executed, as it is called with the same + * // mutualExclude + * }) + * }) + * + * @return {Function} A mutual exclude function + * @public + */ export function createMutualExclude () { var token = true return function mutualExclude (f) { diff --git a/src/Util/relativePosition.js b/src/Util/relativePosition.js index 31974fcf..05b6b5d9 100644 --- a/src/Util/relativePosition.js +++ b/src/Util/relativePosition.js @@ -1,6 +1,8 @@ import ID from './ID/ID.js' import RootID from './ID/RootID.js' +// TODO: Implement function to describe ranges + /** * A relative position that is based on the Yjs model. In contrast to an * absolute position (position by index), the relative position can be diff --git a/src/Util/simpleDiff.js b/src/Util/simpleDiff.js index adf3222b..d5154eb8 100644 --- a/src/Util/simpleDiff.js +++ b/src/Util/simpleDiff.js @@ -11,16 +11,16 @@ * a === b // values match * * @typedef {Object} SimpleDiff - * @property {NaturalNumber} pos The index where changes were applied - * @property {NaturalNumber} delete The number of characters to delete starting + * @property {Number} pos The index where changes were applied + * @property {Number} delete The number of characters to delete starting * at `index`. * @property {String} insert The new text to insert at `index` after applying * `delete` */ /** - * Create a diff between two strings. This diff implementation is intentionally - * not very smart. + * Create a diff between two strings. This diff implementation is highly + * efficient, but not very sophisticated. * * @public * @param {String} a The old version of the string diff --git a/src/Util/structReferences.js b/src/Util/structReferences.js index ff9164c7..4101e455 100644 --- a/src/Util/structReferences.js +++ b/src/Util/structReferences.js @@ -12,15 +12,30 @@ import ItemEmbed from '../Struct/ItemEmbed.js' const structs = new Map() const references = new Map() +/** + * Register a new Yjs types. The same type must be defined with the same + * reference on all clients! + * + * @param {Number} reference + * @param {class} structConstructor + * + * @public + */ export function registerStruct (reference, structConstructor) { structs.set(reference, structConstructor) references.set(structConstructor, reference) } +/** + * @private + */ export function getStruct (reference) { return structs.get(reference) } +/** + * @private + */ export function getStructReference (typeConstructor) { return references.get(typeConstructor) } diff --git a/src/Util/writeJSONToType.js b/src/Util/writeJSONToType.js deleted file mode 100644 index 5dad6d29..00000000 --- a/src/Util/writeJSONToType.js +++ /dev/null @@ -1,33 +0,0 @@ - -import YMap from '../Types/YMap' -import YArray from '../Types/YArray' - -export function writeObjectToYMap (object, type) { - for (var key in object) { - var val = object[key] - if (Array.isArray(val)) { - type.set(key, YArray) - writeArrayToYArray(val, type.get(key)) - } else if (typeof val === 'object') { - type.set(key, YMap) - writeObjectToYMap(val, type.get(key)) - } else { - type.set(key, val) - } - } -} - -export function writeArrayToYArray (array, type) { - for (var i = array.length - 1; i >= 0; i--) { - var val = array[i] - if (Array.isArray(val)) { - type.insert(0, [YArray]) - writeArrayToYArray(val, type.get(0)) - } else if (typeof val === 'object') { - type.insert(0, [YMap]) - writeObjectToYMap(val, type.get(0)) - } else { - type.insert(0, [val]) - } - } -} diff --git a/src/Y.js b/src/Y.js index ae81f937..ca7b63ef 100644 --- a/src/Y.js +++ b/src/Y.js @@ -1,19 +1,13 @@ import DeleteStore from './Store/DeleteStore.js' import OperationStore from './Store/OperationStore.js' import StateStore from './Store/StateStore.js' -import { generateUserID } from './Util/generateUserID.js' +import { generateRandomUint32 } from './Util/generateRandomUint32.js' import RootID from './Util/ID/RootID.js' import NamedEventHandler from './Util/NamedEventHandler.js' import Transaction from './Transaction.js' export { default as DomBinding } from './Bindings/DomBinding/DomBinding.js' -/** - * A positive natural number including zero: 0, 1, 2, .. - * - * @typedef {number} NaturalNumber - */ - /** * Anything that can be encoded with `JSON.stringify` and can be decoded with * `JSON.parse`. @@ -47,7 +41,7 @@ export default class Y extends NamedEventHandler { this._contentReady = false this._opts = opts if (typeof opts.userID !== 'number') { - this.userID = generateUserID() + this.userID = generateRandomUint32() } else { this.userID = opts.userID } diff --git a/test/encode-decode.tests.js b/test/encode-decode.tests.js index f4b4ee45..e032d06b 100644 --- a/test/encode-decode.tests.js +++ b/test/encode-decode.tests.js @@ -1,7 +1,7 @@ import { test } from '../node_modules/cutest/cutest.mjs' import BinaryEncoder from '../src/Util/Binary/Encoder.js' import BinaryDecoder from '../src/Util/Binary/Decoder.js' -import { generateUserID } from '../src/Util/generateUserID.js' +import { generateRandomUint32 } from '../src/Util/generateRandomUint32.js' import Chance from 'chance' function testEncoding (t, write, read, val) { @@ -43,7 +43,7 @@ test('varUint random', async function varUintRandom (t) { test('varUint random user id', async function varUintRandomUserId (t) { t.getSeed() // enforces that this test is repeated - testEncoding(t, writeVarUint, readVarUint, generateUserID()) + testEncoding(t, writeVarUint, readVarUint, generateRandomUint32()) }) const writeVarString = (encoder, val) => encoder.writeVarString(val) diff --git a/tests-lib/helper.js b/tests-lib/helper.js index d4bf1ea4..26a50f08 100644 --- a/tests-lib/helper.js +++ b/tests-lib/helper.js @@ -156,7 +156,7 @@ export async function initArrays (t, opts) { users: [] } var chance = opts.chance || new Chance(t.getSeed() * 1000000000) - var conn = Object.assign({ room: 'debugging_' + t.name, generateUserId: false, testContext: t, chance }, connector) + var conn = Object.assign({ room: 'debugging_' + t.name, testContext: t, chance }, connector) for (let i = 0; i < opts.users; i++) { let connOpts if (i === 0) {