Compare commits

...

10 Commits

Author SHA1 Message Date
Kevin Jahns
805acbb9f5 13.0.0-80 2019-04-26 19:55:14 +02:00
Kevin Jahns
32c4c09072 update parent._map when splitting an item 2019-04-26 19:54:00 +02:00
Kevin Jahns
8c5a06bbf8 fix gc when item is deleted in observer call 2019-04-26 18:37:38 +02:00
Kevin Jahns
a336cc167c order observer and transaction cleanups after one another 2019-04-26 13:31:00 +02:00
Kevin Jahns
21d86cd2be Delete all children of ItemType when it is deleted 2019-04-26 12:29:28 +02:00
Kevin Jahns
1d0f9faa91 AbstractItem.mergeWith helper outsourced into separate function 2019-04-24 18:10:33 +02:00
Kevin Jahns
45237571b7 gc more efficiently 2019-04-23 20:51:32 +02:00
Kevin Jahns
bb6f6cd141 13.0.0-79 2019-04-20 00:03:30 +02:00
Kevin Jahns
729c1f16b8 fix test provider 2019-04-20 00:02:40 +02:00
Kevin Jahns
b6059704aa update dependencies 2019-04-20 00:00:09 +02:00
26 changed files with 1703 additions and 2647 deletions

View File

@@ -3,7 +3,7 @@
Yjs is a library for automatic conflict resolution on shared state. It implements an operation-based CRDT and exposes its internal CRDT model as shared types. Shared types are common data types like `Map` or `Array` with superpowers - changes are automatically distributed to other peers and merged without merge conflicts.
Yjs is **network agnostic** (p2p!), supports many existing **rich text editors**, **offline editing**, **version snapshots**, **shared cursors**, and encodes update messages using **binary protocol encoding**.
Yjs is **network agnostic** (p2p!), supports many existing **rich text editors**, **offline editing**, **version snapshots**, **shared cursors**, and encodes update messages using **binary protocol encoding**.
* Chat: [https://gitter.im/y-js/yjs](https://gitter.im/y-js/yjs)
* Demos: [https://yjs.website/tutorial-prosemirror.html](https://yjs.website/tutorial-prosemirror.html)

View File

@@ -76,11 +76,17 @@
img[ychange_state='removed'] {
padding: 2px;
}
.y-connect-btn {
position: absolute;
top: 20px;
right: 20px;
}
</style>
</head>
<body>
<p>This example shows how to bind a YXmlFragment type to a <a href="http://prosemirror.net">Prosemirror</a> editor.</p>
<p>The content of this editor is shared with every client who visits this domain.</p>
<button type="button" class="y-connect-btn">Disconnect</button>
<p>This example shows how to bind a YXmlFragment to a <a href="http://prosemirror.net">Prosemirror</a> editor using <a href="https://github.com/y-js/y-prosemirror">y-prosemirror</a>.</p>
<p>The content of this editor is shared with every client that visits this domain.</p>
<div class="code-html">
<div id="editor" style="margin-bottom: 23px"></div>

View File

@@ -12,8 +12,8 @@ import { exampleSetup } from 'prosemirror-example-setup'
// import { noteHistoryPlugin } from './prosemirror-history.js'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('prosemirror', { gc: false })
const type = ydocument.define('prosemirror', Y.XmlFragment)
const ydocument = provider.get('prosemirror' /*, { gc: false } */)
const type = ydocument.get('prosemirror', Y.XmlFragment)
const prosemirrorView = new EditorView(document.querySelector('#editor'), {
state: EditorState.create({
@@ -22,4 +22,15 @@ const prosemirrorView = new EditorView(document.querySelector('#editor'), {
})
})
const connectBtn = document.querySelector('.y-connect-btn')
connectBtn.addEventListener('click', () => {
if (ydocument.wsconnected) {
ydocument.disconnect()
connectBtn.textContent = 'Connect'
} else {
ydocument.connect()
connectBtn.textContent = 'Disconnect'
}
})
window.example = { provider, ydocument, type, prosemirrorView }

3086
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,21 @@
{
"name": "yjs",
"version": "13.0.0-78",
"version": "13.0.0-80",
"description": "A ",
"main": "./dist/yjs.js",
"module": "./dist/yjs.mjs'",
"module": "./src/index.js",
"sideEffects": false,
"scripts": {
"test": "npm run dist && PRODUCTION=1 node ./dist/tests.js --repitition-time 50 --production",
"test-exhaustive": "npm run lint && npm run dist && node ./dist/tests.js --repitition-time 10000",
"dist": "rm -rf dist examples/build && rollup -c",
"serve-examples": "concurrently 'npm run watch' 'serve examples'",
"watch": "rollup -wc",
"lint": "standard && tsc",
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.v13.md --package ./package.json || true",
"serve-docs": "npm run docs && serve ./docs/",
"postversion": "npm run lint && PRODUCTION=1 npm run dist && node ./dist/tests.js --repitition-time 1000",
"preversion": "PRODUCTION=1 npm run dist && node ./dist/tests.js --repitition-time 1000",
"postversion": "git push && git push --tags",
"debug": "concurrently 'live-server --port=3443 --entry-file=test.html' 'npm run watch'",
"trace-deopt": "clear && rollup -c && node --trace-deopt dist/test.js",
"trace-opt": "clear && rollup -c && node --trace-opt dist/test.js"
@@ -53,13 +55,12 @@
},
"homepage": "http://y-js.org",
"dependencies": {
"lib0": "0.0.0"
"lib0": "0.0.2"
},
"devDependencies": {
"y-protocols": "0.0.2",
"codemirror": "^5.42.0",
"concurrently": "^3.6.1",
"esdoc": "^1.1.0",
"esdoc-standard-plugin": "^1.0.0",
"jsdoc": "^3.5.5",
"live-server": "^1.2.1",
"prosemirror-example-setup": "^1.0.1",

View File

@@ -86,7 +86,7 @@ export default [{
commonjs()
]
}, {
input: ['./examples/codemirror.js', './examples/textarea.js'], // './examples/quill.js', './examples/dom.js', './examples/prosemirror.js'
input: ['./examples/textarea.js', './examples/prosemirror.js'], // './examples/quill.js', './examples/dom.js', './examples/codemirror.js'
output: {
dir: 'examples/build',
format: 'esm',

View File

@@ -9,6 +9,23 @@ export {
YXmlHook as XmlHook,
YXmlElement as XmlElement,
YXmlFragment as XmlFragment,
YXmlEvent,
YMapEvent,
YArrayEvent,
YEvent,
AbstractItem,
AbstractStruct,
GC,
ItemBinary,
ItemDeleted,
ItemEmbed,
ItemFormat,
ItemJSON,
ItemString,
ItemType,
AbstractType,
compareCursors,
Cursor,
createCursorFromTypeOffset,
createCursorFromJSON,
createAbsolutePositionFromCursor,
@@ -22,5 +39,10 @@ export {
readStatesAsMap,
writeStates,
writeModel,
readModel
readModel,
Snapshot,
findRootTypeKey,
typeArrayToArraySnapshot,
typeMapGetSnapshot,
iterateDeletedStructs
} from './internals.js'

View File

@@ -14,6 +14,7 @@ export * from './types/AbstractType.js'
export * from './types/YArray.js'
export * from './types/YMap.js'
export * from './types/YText.js'
export * from './types/YXmlFragment.js'
export * from './types/YXmlElement.js'
export * from './types/YXmlEvent.js'
export * from './types/YXmlHook.js'

View File

@@ -27,6 +27,22 @@ import * as maplib from 'lib0/map.js'
import * as set from 'lib0/set.js'
import * as binary from 'lib0/binary.js'
/**
* @param {AbstractItem} left
* @param {AbstractItem} right
* @return {boolean} If true, right is removed from the linked list and should be discarded
*/
export const mergeItemWith = (left, right) => {
if (compareIDs(right.origin, left.lastId) && left.right === right && compareIDs(left.rightOrigin, right.rightOrigin)) {
left.right = right.right
if (left.right !== null) {
left.right.left = left
}
return true
}
return false
}
/**
* Split leftItem into two items
* @param {Transaction} transaction
@@ -60,6 +76,10 @@ export const splitItem = (transaction, leftItem, diff) => {
}
// right is more specific.
transaction._mergeStructs.add(rightItem.id)
// update parent._map
if (rightItem.parentSub !== null && rightItem.right === null) {
rightItem.parent._map.set(rightItem.parentSub, rightItem)
}
return rightItem
}
@@ -376,22 +396,6 @@ export class AbstractItem extends AbstractStruct {
throw new Error('unimplemented')
}
/**
* @param {AbstractItem} right
* @return {boolean}
*
* @private
*/
mergeWith (right) {
if (compareIDs(right.origin, this.lastId) && this.right === right && compareIDs(this.rightOrigin, right.rightOrigin)) {
this.right = right.right
if (this.right !== null) {
this.right.left = this
}
return true
}
return false
}
/**
* Mark this Item as deleted.
*
@@ -421,12 +425,14 @@ export class AbstractItem extends AbstractStruct {
/**
* @param {Transaction} transaction
* @param {StructStore} store
* @param {boolean} parentGCd
*
* @private
*/
gc (transaction, store) {
gc (transaction, store, parentGCd) {
this.delete(transaction) // @todo: shouldn't be necessary
let r
if (this.parent._item !== null && this.parent._item.deleted) {
if (parentGCd) {
r = new GC(this.id, this.length)
} else {
r = new ItemDeleted(this.id, this.left, this.origin, this.right, this.rightOrigin, this.parent, this.parentSub, this.length)
@@ -610,7 +616,9 @@ export const computeItemParams = (transaction, store, leftid, rightid, parentid,
case GC:
break
default:
parent = parentItem.type
if (!parentItem.deleted) {
parent = parentItem.type
}
}
} else if (parentYKey !== null) {
parent = transaction.y.get(parentYKey)

View File

@@ -7,6 +7,7 @@ import {
GC,
splitItem,
addToDeleteSet,
mergeItemWith,
Y, StructStore, Transaction, ID, AbstractType // eslint-disable-line
} from '../internals.js'
@@ -78,12 +79,25 @@ export class ItemDeleted extends AbstractItem {
* @return {boolean}
*/
mergeWith (right) {
if (super.mergeWith(right)) {
if (mergeItemWith(this, right)) {
this._len += right._len
return true
}
return false
}
/**
* @param {Transaction} transaction
* @param {StructStore} store
* @param {boolean} parentGCd
*
* @private
*/
gc (transaction, store, parentGCd) {
if (parentGCd) {
super.gc(transaction, store, parentGCd)
}
}
/**
* @param {encoding.Encoder} encoder
* @param {number} offset

View File

@@ -6,6 +6,7 @@ import {
splitItem,
changeItemRefOffset,
GC,
mergeItemWith,
Transaction, StructStore, Y, ID, AbstractType // eslint-disable-line
} from '../internals.js'
@@ -74,7 +75,7 @@ export class ItemJSON extends AbstractItem {
* @return {boolean}
*/
mergeWith (right) {
if (super.mergeWith(right)) {
if (mergeItemWith(this, right)) {
this.content = this.content.concat(right.content)
return true
}

View File

@@ -6,6 +6,7 @@ import {
splitItem,
changeItemRefOffset,
GC,
mergeItemWith,
Transaction, StructStore, Y, ID, AbstractType // eslint-disable-line
} from '../internals.js'
@@ -73,7 +74,7 @@ export class ItemString extends AbstractItem {
* @return {boolean}
*/
mergeWith (right) {
if (super.mergeWith(right)) {
if (mergeItemWith(this, right)) {
this.string += right.string
return true
}

View File

@@ -100,10 +100,21 @@ export class ItemType extends AbstractItem {
* @private
*/
delete (transaction) {
super.delete(transaction)
transaction.changed.delete(this.type)
transaction.changedParentTypes.delete(this.type)
this.gcChildren(transaction, transaction.y.store)
if (!this.deleted) {
super.delete(transaction)
let item = this.type._start
while (item !== null) {
if (!item.deleted) {
item.delete(transaction)
}
item = item.right
}
this.type._map.forEach(item => {
item.delete(transaction)
})
transaction.changed.delete(this.type)
transaction.changedParentTypes.delete(this.type)
}
}
/**
@@ -113,13 +124,13 @@ export class ItemType extends AbstractItem {
gcChildren (transaction, store) {
let item = this.type._start
while (item !== null) {
item.gc(transaction, store)
item.gc(transaction, store, true)
item = item.right
}
this.type._start = null
this.type._map.forEach(item => {
while (item !== null) {
item.gc(transaction, store)
item.gc(transaction, store, true)
// @ts-ignore
item = item.left
}
@@ -130,10 +141,11 @@ export class ItemType extends AbstractItem {
/**
* @param {Transaction} transaction
* @param {StructStore} store
* @param {boolean} parentGCd
*/
gc (transaction, store) {
super.gc(transaction, store)
gc (transaction, store, parentGCd) {
this.gcChildren(transaction, store)
super.gc(transaction, store, parentGCd)
}
}

View File

@@ -197,6 +197,29 @@ export const typeArrayToArray = type => {
return cs
}
/**
* @param {AbstractType<any>} type
* @param {Snapshot} snapshot
* @return {Array<any>}
*
* @private
* @function
*/
export const typeArrayToArraySnapshot = (type, snapshot) => {
const cs = []
let n = type._start
while (n !== null) {
if (n.countable && isVisible(n, snapshot)) {
const c = n.getContent()
for (let i = 0; i < c.length; i++) {
cs.push(c[i])
}
}
n = n.right
}
return cs
}
/**
* Executes a provided function on once on overy element of this YArray.
*
@@ -431,13 +454,10 @@ export const typeArrayDelete = (transaction, parent, index, length) => {
if (length === 0) { return }
let n = parent._start
// compute the first item to be deleted
for (; n !== null; n = n.right) {
for (; n !== null && index > 0; n = n.right) {
if (!n.deleted && n.countable) {
if (index <= n.length) {
if (index < n.length && index > 0) {
n = getItemCleanStart(transaction, transaction.y.store, createID(n.id.client, n.id.clock + index))
}
break
if (index < n.length) {
getItemCleanStart(transaction, transaction.y.store, createID(n.id.client, n.id.clock + index))
}
index -= n.length
}

View File

@@ -147,7 +147,7 @@ const insertNegatedAttributes = (transaction, parent, left, right, negatedAttrib
left = new ItemFormat(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, key, val)
left.integrate(transaction)
}
return {left, right}
return { left, right }
}
/**
@@ -215,10 +215,10 @@ const insertAttributes = (transaction, parent, left, right, currentAttributes, a
// insert format-start items
for (let key in attributes) {
const val = attributes[key]
const currentVal = currentAttributes.get(key)
const currentVal = currentAttributes.get(key) || null
if (currentVal !== val) {
// save negated attribute (set null if currentVal undefined)
negatedAttributes.set(key, currentVal || null)
negatedAttributes.set(key, currentVal)
left = new ItemFormat(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, key, val)
left.integrate(transaction)
}
@@ -617,10 +617,11 @@ export class YText extends AbstractType {
constructor (string) {
super()
/**
* @type {Array<string>?}
* Array of pending operations on this type
* @type {Array<function():void>?}
* @private
*/
this._prelimContent = string !== undefined ? [string] : []
this._pending = string !== undefined ? [() => this.insert(0, string)] : []
}
get length () {
@@ -635,9 +636,13 @@ export class YText extends AbstractType {
*/
_integrate (y, item) {
super._integrate(y, item)
// @ts-ignore this._prelimContent is still defined
this.insert(0, this._prelimContent.join(''))
this._prelimContent = null
try {
// @ts-ignore this._prelimContent is still defined
this._pending.forEach(f => f())
} catch (e) {
console.error(e)
}
this._pending = null
}
/**
@@ -652,10 +657,6 @@ export class YText extends AbstractType {
callTypeObservers(this, transaction, new YTextEvent(this, transaction))
}
toDom () {
return document.createTextNode(this.toString())
}
/**
* Returns the unformatted string representation of this YText type.
*
@@ -677,40 +678,6 @@ export class YText extends AbstractType {
return str
}
toDomString () {
// @ts-ignore
return this.toDelta().map(delta => {
const nestedNodes = []
for (let nodeName in delta.attributes) {
const attrs = []
for (let key in delta.attributes[nodeName]) {
attrs.push({ key, value: delta.attributes[nodeName][key] })
}
// sort attributes to get a unique order
attrs.sort((a, b) => a.key < b.key ? -1 : 1)
nestedNodes.push({ nodeName, attrs })
}
// sort node order to get a unique order
nestedNodes.sort((a, b) => a.nodeName < b.nodeName ? -1 : 1)
// now convert to dom string
let str = ''
for (let i = 0; i < nestedNodes.length; i++) {
const node = nestedNodes[i]
str += `<${node.nodeName}`
for (let j = 0; j < node.attrs.length; j++) {
const attr = node.attrs[i]
str += ` ${attr.key}="${attr.value}"`
}
str += '>'
}
str += delta.insert
for (let i = nestedNodes.length - 1; i >= 0; i--) {
str += `</${nestedNodes[i].nodeName}>`
}
return str
})
}
/**
* Apply a {@link Delta} on this shared YText type.
*
@@ -737,6 +704,9 @@ export class YText extends AbstractType {
}
}
})
} else {
// @ts-ignore
this._pending.push(() => this.applyDelta(delta))
}
}
@@ -836,9 +806,12 @@ export class YText extends AbstractType {
const y = this._y
if (y !== null) {
transact(y, transaction => {
const {left, right, currentAttributes} = findPosition(transaction, y.store, this, index)
const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index)
insertText(transaction, this, left, right, currentAttributes, text, attributes)
})
} else {
// @ts-ignore
this._pending.push(() => this.insert(index, text, attributes))
}
}
@@ -862,6 +835,9 @@ export class YText extends AbstractType {
const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index)
insertText(transaction, this, left, right, currentAttributes, embed, attributes)
})
} else {
// @ts-ignore
this._pending.push(() => this.insertEmbed(index, embed, attributes))
}
}
@@ -883,6 +859,9 @@ export class YText extends AbstractType {
const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index)
deleteText(transaction, left, right, currentAttributes, length)
})
} else {
// @ts-ignore
this._pending.push(() => this.delete(index, length))
}
}
@@ -906,6 +885,9 @@ export class YText extends AbstractType {
}
formatText(transaction, this, left, right, currentAttributes, length, attributes)
})
} else {
// @ts-ignore
this._pending.push(() => this.format(index, length, attributes))
}
}

View File

@@ -1,244 +1,19 @@
/**
* @module YXml
*/
import {
YXmlEvent,
AbstractType,
typeArrayMap,
typeArrayForEach,
YXmlFragment,
transact,
typeMapDelete,
typeMapSet,
typeMapGet,
typeMapGetAll,
typeArrayInsertGenerics,
typeArrayDelete,
typeMapSet,
typeMapDelete,
typeArrayForEach,
YXmlElementRefID,
callTypeObservers,
transact,
Y, Transaction, ItemType, YXmlText, YXmlHook, Snapshot // eslint-disable-line
Snapshot, Y, ItemType // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
/**
* Define the elements to which a set of CSS queries apply.
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|CSS_Selectors}
*
* @example
* query = '.classSelector'
* query = 'nodeSelector'
* query = '#idSelector'
*
* @typedef {string} CSS_Selector
*/
/**
* Dom filter function.
*
* @callback domFilter
* @param {string} nodeName The nodeName of the element
* @param {Map} attributes The map of attributes.
* @return {boolean} Whether to include the Dom node in the YXmlElement.
*/
/**
* Represents a subset of the nodes of a YXmlElement / YXmlFragment and a
* position within them.
*
* Can be created with {@link YXmlFragment#createTreeWalker}
*
* @public
* @implements {IterableIterator}
*/
export class YXmlTreeWalker {
/**
* @param {YXmlFragment | YXmlElement} root
* @param {function(AbstractType<any>):boolean} [f]
*/
constructor (root, f = () => true) {
this._filter = f
this._root = root
/**
* @type {ItemType | null}
*/
// @ts-ignore
this._currentNode = root._start
this._firstCall = true
}
[Symbol.iterator] () {
return this
}
/**
* Get the next node.
*
* @return {IteratorResult<YXmlElement|YXmlText|YXmlHook>} The next node.
*
* @public
*/
next () {
let n = this._currentNode
if (n !== null && (!this._firstCall || n.deleted || !this._filter(n.type))) { // if first call, we check if we can use the first item
do {
if (!n.deleted && (n.type.constructor === YXmlElement || n.type.constructor === YXmlFragment) && n.type._start !== null) {
// walk down in the tree
// @ts-ignore
n = n.type._start
} else {
// walk right or up in the tree
while (n !== null) {
if (n.right !== null) {
// @ts-ignore
n = n.right
break
} else if (n.parent === this._root) {
n = null
} else {
n = n.parent._item
}
}
}
} while (n !== null && (n.deleted || !this._filter(n.type)))
}
this._firstCall = false
this._currentNode = n
if (n === null) {
// @ts-ignore return undefined if done=true (the expected result)
return { value: undefined, done: true }
}
// @ts-ignore
return { value: n.type, done: false }
}
}
/**
* Represents a list of {@link YXmlElement}.and {@link YXmlText} types.
* A YxmlFragment is similar to a {@link YXmlElement}, but it does not have a
* nodeName and it does not have attributes. Though it can be bound to a DOM
* element - in this case the attributes and the nodeName are not shared.
*
* @public
* @extends AbstractType<YXmlEvent>
*/
export class YXmlFragment extends AbstractType {
/**
* Create a subtree of childNodes.
*
* @example
* const walker = elem.createTreeWalker(dom => dom.nodeName === 'div')
* for (let node in walker) {
* // `node` is a div node
* nop(node)
* }
*
* @param {function(AbstractType<any>):boolean} 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 {YXmlTreeWalker} A subtree and a position within it.
*
* @public
*/
createTreeWalker (filter) {
return new YXmlTreeWalker(this, filter)
}
/**
* Returns the first YXmlElement that matches the query.
* Similar to DOM's {@link querySelector}.
*
* Query support:
* - tagname
* TODO:
* - id
* - attribute
*
* @param {CSS_Selector} query The query on the children.
* @return {YXmlElement|YXmlText|YXmlHook|null} The first element that matches the query or null.
*
* @public
*/
querySelector (query) {
query = query.toUpperCase()
// @ts-ignore
const iterator = new YXmlTreeWalker(this, element => element.nodeName === query)
const next = iterator.next()
if (next.done) {
return null
} else {
return next.value
}
}
/**
* Returns all YXmlElements that match the query.
* Similar to Dom's {@link querySelectorAll}.
*
* @todo Does not yet support all queries. Currently only query by tagName.
*
* @param {CSS_Selector} query The query on the children
* @return {Array<YXmlElement|YXmlText|YXmlHook|null>} The elements that match this query.
*
* @public
*/
querySelectorAll (query) {
query = query.toUpperCase()
// @ts-ignore
return Array.from(new YXmlTreeWalker(this, element => element.nodeName === query))
}
/**
* Creates YXmlEvent and calls observers.
* @private
*
* @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*/
_callObserver (transaction, parentSubs) {
callTypeObservers(this, transaction, new YXmlEvent(this, parentSubs, transaction))
}
toString () {
return this.toDomString()
}
/**
* Get the string representation of all the children of this YXmlFragment.
*
* @return {string} The string representation of all children.
*/
toDomString () {
return typeArrayMap(this, xml => xml.toDomString()).join('')
}
/**
* 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<string, any>} [hooks={}] Optional property to customize how hooks
* are presented in the DOM
* @param {any} [binding] You should not set this property. This is
* used if DomBinding wants to create a
* association to the created DOM type.
* @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
*
* @public
*/
toDom (_document = document, hooks = {}, binding) {
const fragment = _document.createDocumentFragment()
if (binding !== undefined) {
binding._createAssociation(fragment, this)
}
typeArrayForEach(this, xmlType => {
fragment.insertBefore(xmlType.toDom(_document, hooks, binding), null)
})
return fragment
}
}
/**
* An YXmlElement imitates the behavior of a
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}.
@@ -250,11 +25,6 @@ export class YXmlElement extends YXmlFragment {
constructor (nodeName = 'UNDEFINED') {
super()
this.nodeName = nodeName.toUpperCase()
/**
* @type {Array<any>|null}
* @private
*/
this._prelimContent = []
/**
* @type {Map<string, any>|null}
* @private
@@ -389,44 +159,6 @@ export class YXmlElement extends YXmlFragment {
return typeMapGetAll(this)
}
/**
* Inserts new content at an index.
*
* @example
* // Insert character 'a' at position 0
* xml.insert(0, [new Y.XmlText('text')])
*
* @param {number} index The index to insert content at
* @param {Array<YXmlElement|YXmlText>} content The array of content
*/
insert (index, content) {
if (this._y !== null) {
transact(this._y, transaction => {
typeArrayInsertGenerics(transaction, this, index, content)
})
} else {
// @ts-ignore _prelimContent is defined because this is not yet integrated
this._prelimContent.splice(index, 0, ...content)
}
}
/**
* Deletes elements starting from an index.
*
* @param {number} index Index at which to start deleting elements
* @param {number} [length=1] The number of elements to remove. Defaults to 1.
*/
delete (index, length = 1) {
if (this._y !== null) {
transact(this._y, transaction => {
typeArrayDelete(transaction, this, index, length)
})
} else {
// @ts-ignore _prelimContent is defined because this is not yet integrated
this._prelimContent.splice(index, length)
}
}
/**
* Creates a Dom Element that mirrors this YXmlElement.
*
@@ -480,11 +212,3 @@ export class YXmlElement extends YXmlFragment {
* @function
*/
export const readYXmlElement = decoder => new YXmlElement(decoding.readVarString(decoder))
/**
* @param {decoding.Decoder} decoder
* @return {YXmlFragment}
*
* @private
* @function
*/
export const readYXmlFragment = decoder => new YXmlFragment()

313
src/types/YXmlFragment.js Normal file
View File

@@ -0,0 +1,313 @@
/**
* @module YXml
*/
import {
YXmlEvent,
YXmlElement,
AbstractType,
typeArrayMap,
typeArrayForEach,
typeArrayInsertGenerics,
typeArrayDelete,
typeArrayToArray,
YXmlFragmentRefID,
callTypeObservers,
transact,
Transaction, ItemType, YXmlText, YXmlHook, Snapshot // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js' // eslint-disable-line
/**
* Define the elements to which a set of CSS queries apply.
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|CSS_Selectors}
*
* @example
* query = '.classSelector'
* query = 'nodeSelector'
* query = '#idSelector'
*
* @typedef {string} CSS_Selector
*/
/**
* Dom filter function.
*
* @callback domFilter
* @param {string} nodeName The nodeName of the element
* @param {Map} attributes The map of attributes.
* @return {boolean} Whether to include the Dom node in the YXmlElement.
*/
/**
* Represents a subset of the nodes of a YXmlElement / YXmlFragment and a
* position within them.
*
* Can be created with {@link YXmlFragment#createTreeWalker}
*
* @public
* @implements {IterableIterator}
*/
export class YXmlTreeWalker {
/**
* @param {YXmlFragment | YXmlElement} root
* @param {function(AbstractType<any>):boolean} [f]
*/
constructor (root, f = () => true) {
this._filter = f
this._root = root
/**
* @type {ItemType | null}
*/
// @ts-ignore
this._currentNode = root._start
this._firstCall = true
}
[Symbol.iterator] () {
return this
}
/**
* Get the next node.
*
* @return {IteratorResult<YXmlElement|YXmlText|YXmlHook>} The next node.
*
* @public
*/
next () {
let n = this._currentNode
if (n !== null && (!this._firstCall || n.deleted || !this._filter(n.type))) { // if first call, we check if we can use the first item
do {
if (!n.deleted && (n.type.constructor === YXmlElement || n.type.constructor === YXmlFragment) && n.type._start !== null) {
// walk down in the tree
// @ts-ignore
n = n.type._start
} else {
// walk right or up in the tree
while (n !== null) {
if (n.right !== null) {
// @ts-ignore
n = n.right
break
} else if (n.parent === this._root) {
n = null
} else {
n = n.parent._item
}
}
}
} while (n !== null && (n.deleted || !this._filter(n.type)))
}
this._firstCall = false
this._currentNode = n
if (n === null) {
// @ts-ignore return undefined if done=true (the expected result)
return { value: undefined, done: true }
}
// @ts-ignore
return { value: n.type, done: false }
}
}
/**
* Represents a list of {@link YXmlElement}.and {@link YXmlText} types.
* A YxmlFragment is similar to a {@link YXmlElement}, but it does not have a
* nodeName and it does not have attributes. Though it can be bound to a DOM
* element - in this case the attributes and the nodeName are not shared.
*
* @public
* @extends AbstractType<YXmlEvent>
*/
export class YXmlFragment extends AbstractType {
constructor () {
super()
/**
* @type {Array<any>|null}
* @private
*/
this._prelimContent = []
}
/**
* Create a subtree of childNodes.
*
* @example
* const walker = elem.createTreeWalker(dom => dom.nodeName === 'div')
* for (let node in walker) {
* // `node` is a div node
* nop(node)
* }
*
* @param {function(AbstractType<any>):boolean} 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 {YXmlTreeWalker} A subtree and a position within it.
*
* @public
*/
createTreeWalker (filter) {
return new YXmlTreeWalker(this, filter)
}
/**
* Returns the first YXmlElement that matches the query.
* Similar to DOM's {@link querySelector}.
*
* Query support:
* - tagname
* TODO:
* - id
* - attribute
*
* @param {CSS_Selector} query The query on the children.
* @return {YXmlElement|YXmlText|YXmlHook|null} The first element that matches the query or null.
*
* @public
*/
querySelector (query) {
query = query.toUpperCase()
// @ts-ignore
const iterator = new YXmlTreeWalker(this, element => element.nodeName === query)
const next = iterator.next()
if (next.done) {
return null
} else {
return next.value
}
}
/**
* Returns all YXmlElements that match the query.
* Similar to Dom's {@link querySelectorAll}.
*
* @todo Does not yet support all queries. Currently only query by tagName.
*
* @param {CSS_Selector} query The query on the children
* @return {Array<YXmlElement|YXmlText|YXmlHook|null>} The elements that match this query.
*
* @public
*/
querySelectorAll (query) {
query = query.toUpperCase()
// @ts-ignore
return Array.from(new YXmlTreeWalker(this, element => element.nodeName === query))
}
/**
* Creates YXmlEvent and calls observers.
* @private
*
* @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*/
_callObserver (transaction, parentSubs) {
callTypeObservers(this, transaction, new YXmlEvent(this, parentSubs, transaction))
}
toString () {
return this.toDomString()
}
/**
* Get the string representation of all the children of this YXmlFragment.
*
* @return {string} The string representation of all children.
*/
toDomString () {
return typeArrayMap(this, xml => xml.toDomString()).join('')
}
/**
* 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<string, any>} [hooks={}] Optional property to customize how hooks
* are presented in the DOM
* @param {any} [binding] You should not set this property. This is
* used if DomBinding wants to create a
* association to the created DOM type.
* @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
*
* @public
*/
toDom (_document = document, hooks = {}, binding) {
const fragment = _document.createDocumentFragment()
if (binding !== undefined) {
binding._createAssociation(fragment, this)
}
typeArrayForEach(this, xmlType => {
fragment.insertBefore(xmlType.toDom(_document, hooks, binding), null)
})
return fragment
}
/**
* Inserts new content at an index.
*
* @example
* // Insert character 'a' at position 0
* xml.insert(0, [new Y.XmlText('text')])
*
* @param {number} index The index to insert content at
* @param {Array<YXmlElement|YXmlText>} content The array of content
*/
insert (index, content) {
if (this._y !== null) {
transact(this._y, transaction => {
typeArrayInsertGenerics(transaction, this, index, content)
})
} else {
// @ts-ignore _prelimContent is defined because this is not yet integrated
this._prelimContent.splice(index, 0, ...content)
}
}
/**
* Deletes elements starting from an index.
*
* @param {number} index Index at which to start deleting elements
* @param {number} [length=1] The number of elements to remove. Defaults to 1.
*/
delete (index, length = 1) {
if (this._y !== null) {
transact(this._y, transaction => {
typeArrayDelete(transaction, this, index, length)
})
} else {
// @ts-ignore _prelimContent is defined because this is not yet integrated
this._prelimContent.splice(index, length)
}
}
/**
* Transforms this YArray to a JavaScript Array.
*
* @return {Array<YXmlElement|YXmlText|YXmlHook>}
*/
toArray () {
return typeArrayToArray(this)
}
/**
* Transform the properties of this type to binary and write it to an
* BinaryEncoder.
*
* This is called when this Item is sent to a remote peer.
*
* @private
* @param {encoding.Encoder} encoder The encoder to write data to.
*/
_write (encoder) {
encoding.writeVarUint(encoder, YXmlFragmentRefID)
}
}
/**
* @param {decoding.Decoder} decoder
* @return {YXmlFragment}
*
* @private
* @function
*/
export const readYXmlFragment = decoder => new YXmlFragment()

View File

@@ -31,6 +31,41 @@ export class YXmlText extends YText {
}
return dom
}
toDomString () {
// @ts-ignore
return this.toDelta().map(delta => {
const nestedNodes = []
for (let nodeName in delta.attributes) {
const attrs = []
for (let key in delta.attributes[nodeName]) {
attrs.push({ key, value: delta.attributes[nodeName][key] })
}
// sort attributes to get a unique order
attrs.sort((a, b) => a.key < b.key ? -1 : 1)
nestedNodes.push({ nodeName, attrs })
}
// sort node order to get a unique order
nestedNodes.sort((a, b) => a.nodeName < b.nodeName ? -1 : 1)
// now convert to dom string
let str = ''
for (let i = 0; i < nestedNodes.length; i++) {
const node = nestedNodes[i]
str += `<${node.nodeName}`
for (let j = 0; j < node.attrs.length; j++) {
const attr = node.attrs[i]
str += ` ${attr.key}="${attr.value}"`
}
str += '>'
}
str += delta.insert
for (let i = nestedNodes.length - 1; i >= 0; i--) {
str += `</${nestedNodes[i].nodeName}>`
}
return str
}).join('')
}
/**
* @param {encoding.Encoder} encoder
*

View File

@@ -3,7 +3,7 @@ import {
findIndexSS,
createID,
getState,
AbstractItem, StructStore, Transaction, ID // eslint-disable-line
AbstractStruct, AbstractItem, StructStore, Transaction, ID // eslint-disable-line
} from '../internals.js'
import * as math from 'lib0/math.js'
@@ -11,9 +11,6 @@ import * as map from 'lib0/map.js'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
/**
* @private
*/
class DeleteItem {
/**
* @param {number} clock
@@ -37,8 +34,6 @@ class DeleteItem {
* - This DeleteSet is send to other clients
* - We do not create a DeleteSet when we send a sync message. The DeleteSet message is created directly from StructStore
* - We read a DeleteSet as part of a sync/update message. In this case the DeleteSet is already sorted and merged.
*
* @private
*/
export class DeleteSet {
constructor () {
@@ -50,6 +45,33 @@ export class DeleteSet {
}
}
/**
* Iterate over all structs that were deleted.
*
* This function expects that the deletes structs are not deleted. Hence, you can
* probably only use it in type observes and `afterTransaction` events. But not
* in `afterTransactionCleanup`.
*
* @param {DeleteSet} ds
* @param {StructStore} store
* @param {function(AbstractStruct):void} f
*
* @function
*/
export const iterateDeletedStructs = (ds, store, f) =>
ds.clients.forEach((deletes, clientid) => {
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(clientid))
for (let i = 0; i < deletes.length; i++) {
const del = deletes[i]
let index = findIndexSS(structs, del.clock)
let struct
do {
struct = structs[index++]
f(struct)
} while (index < structs.length && structs[index].id.clock < del.clock + del.len)
}
})
/**
* @param {Array<DeleteItem>} dis
* @param {number} clock

View File

@@ -1,8 +1,7 @@
import {
DeleteSet,
isDeleted,
AbstractItem // eslint-disable-line
DeleteSet, AbstractItem // eslint-disable-line
} from '../internals.js'
export class Snapshot {
@@ -17,7 +16,7 @@ export class Snapshot {
* @type {DeleteSet}
* @private
*/
this.ds = new DeleteSet()
this.ds = ds
/**
* State Map
* @type {Map<number,number>}

View File

@@ -10,7 +10,6 @@ import {
findIndexSS,
callEventHandlerListeners,
AbstractItem,
ItemDeleted,
ID, AbstractType, AbstractStruct, YEvent, Y // eslint-disable-line
} from '../internals.js'
@@ -45,8 +44,9 @@ import * as math from 'lib0/math.js'
export class Transaction {
/**
* @param {Y} y
* @param {any} origin
*/
constructor (y) {
constructor (y, origin) {
/**
* The Yjs instance.
* @type {Y}
@@ -90,6 +90,10 @@ export class Transaction {
* @private
*/
this._mergeStructs = new Set()
/**
* @type {any}
*/
this.origin = origin
}
/**
* @type {encoding.Encoder|null}
@@ -124,121 +128,129 @@ export const nextID = transaction => {
*
* @param {Y} y
* @param {function(Transaction):void} f
* @param {any} [origin]
*
* @private
* @function
*/
export const transact = (y, f) => {
export const transact = (y, f, origin = null) => {
const transactionCleanups = y._transactionCleanups
let initialCall = false
if (y._transaction === null) {
initialCall = true
y._transaction = new Transaction(y)
y._transaction = new Transaction(y, origin)
transactionCleanups.push(y._transaction)
y.emit('beforeTransaction', [y._transaction, y])
}
const transaction = y._transaction
try {
f(transaction)
f(y._transaction)
} finally {
if (initialCall) {
y._transaction = null
y.emit('beforeObserverCalls', [transaction, y])
// emit change events on changed types
transaction.changed.forEach((subs, itemtype) => {
itemtype._callObserver(transaction, subs)
})
transaction.changedParentTypes.forEach((events, type) => {
events = events
.filter(event =>
event.target._item === null || !event.target._item.deleted
)
events
.forEach(event => {
event.currentTarget = type
})
// we don't need to check for events.length
// because we know it has at least one element
callEventHandlerListeners(type._dEH, events, transaction)
})
// only call afterTransaction listeners if anything changed
transaction.afterState = getStates(transaction.y.store)
// when all changes & events are processed, emit afterTransaction event
// transaction cleanup
const store = transaction.y.store
const ds = transaction.deleteSet
// replace deleted items with ItemDeleted / GC
sortAndMergeDeleteSet(ds)
y.emit('afterTransaction', [transaction, y])
for (const [client, deleteItems] of ds.clients) {
/**
* @type {Array<AbstractStruct>}
*/
// @ts-ignore
const structs = store.clients.get(client)
for (let di = 0; di < deleteItems.length; di++) {
const deleteItem = deleteItems[di]
for (let si = findIndexSS(structs, deleteItem.clock); si < structs.length; si++) {
const struct = structs[si]
if (deleteItem.clock + deleteItem.len <= struct.id.clock) {
break
}
if (struct.deleted && struct instanceof AbstractItem && (struct.constructor !== ItemDeleted || (struct.parent._item !== null && struct.parent._item.deleted))) {
// check if we can GC
struct.gc(transaction, store)
}
}
}
}
/**
* @param {Array<AbstractStruct>} structs
* @param {number} pos
*/
const tryToMergeWithLeft = (structs, pos) => {
const left = structs[pos - 1]
const right = structs[pos]
if (left.deleted === right.deleted && left.constructor === right.constructor) {
if (left.mergeWith(right)) {
structs.splice(pos, 1)
if (right instanceof AbstractItem && right.parentSub !== null && right.parent._map.get(right.parentSub) === right) {
// @ts-ignore we already did a constructor check above
right.parent._map.set(right.parentSub, left)
}
}
}
}
// on all affected store.clients props, try to merge
for (const [client, clock] of transaction.afterState) {
const beforeClock = transaction.beforeState.get(client) || 0
if (beforeClock !== clock) {
// @todo set after state here
if (initialCall && transactionCleanups[0] === y._transaction) {
// The first transaction ended, now process observer calls.
// Observer call may create new transactions for which we need to call the observers and do cleanup.
// We don't want to nest these calls, so we execute these calls one after another
for (let i = 0; i < transactionCleanups.length; i++) {
const transaction = transactionCleanups[i]
const store = transaction.y.store
const ds = transaction.deleteSet
sortAndMergeDeleteSet(ds)
transaction.afterState = getStates(transaction.y.store)
y._transaction = null
y.emit('beforeObserverCalls', [transaction, y])
// emit change events on changed types
transaction.changed.forEach((subs, itemtype) => {
itemtype._callObserver(transaction, subs)
})
transaction.changedParentTypes.forEach((events, type) => {
events = events
.filter(event =>
event.target._item === null || !event.target._item.deleted
)
events
.forEach(event => {
event.currentTarget = type
})
// we don't need to check for events.length
// because we know it has at least one element
callEventHandlerListeners(type._dEH, events, transaction)
})
y.emit('afterTransaction', [transaction, y])
// replace deleted items with ItemDeleted / GC
for (const [client, deleteItems] of ds.clients) {
/**
* @type {Array<AbstractStruct>}
*/
// @ts-ignore
const structs = store.clients.get(client)
// we iterate from right to left so we can safely remove entries
const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1)
for (let i = structs.length - 1; i >= firstChangePos; i--) {
tryToMergeWithLeft(structs, i)
for (let di = 0; di < deleteItems.length; di++) {
const deleteItem = deleteItems[di]
for (let si = findIndexSS(structs, deleteItem.clock); si < structs.length; si++) {
const struct = structs[si]
if (deleteItem.clock + deleteItem.len <= struct.id.clock) {
break
}
if (struct.deleted && struct instanceof AbstractItem) {
struct.gc(transaction, store, false)
}
}
}
}
}
// try to merge mergeStructs
for (const mid of transaction._mergeStructs) {
const client = mid.client
const clock = mid.clock
/**
* @type {Array<AbstractStruct>}
* @param {Array<AbstractStruct>} structs
* @param {number} pos
*/
// @ts-ignore
const structs = store.clients.get(client)
const replacedStructPos = findIndexSS(structs, clock)
if (replacedStructPos + 1 < structs.length) {
tryToMergeWithLeft(structs, replacedStructPos + 1)
const tryToMergeWithLeft = (structs, pos) => {
const left = structs[pos - 1]
const right = structs[pos]
if (left.deleted === right.deleted && left.constructor === right.constructor) {
if (left.mergeWith(right)) {
structs.splice(pos, 1)
if (right instanceof AbstractItem && right.parentSub !== null && right.parent._map.get(right.parentSub) === right) {
// @ts-ignore we already did a constructor check above
right.parent._map.set(right.parentSub, left)
}
}
}
}
if (replacedStructPos > 0) {
tryToMergeWithLeft(structs, replacedStructPos)
// on all affected store.clients props, try to merge
for (const [client, clock] of transaction.afterState) {
const beforeClock = transaction.beforeState.get(client) || 0
if (beforeClock !== clock) {
/**
* @type {Array<AbstractStruct>}
*/
// @ts-ignore
const structs = store.clients.get(client)
// we iterate from right to left so we can safely remove entries
const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1)
for (let i = structs.length - 1; i >= firstChangePos; i--) {
tryToMergeWithLeft(structs, i)
}
}
}
// try to merge mergeStructs
for (const mid of transaction._mergeStructs) {
const client = mid.client
const clock = mid.clock
/**
* @type {Array<AbstractStruct>}
*/
// @ts-ignore
const structs = store.clients.get(client)
const replacedStructPos = findIndexSS(structs, clock)
if (replacedStructPos + 1 < structs.length) {
tryToMergeWithLeft(structs, replacedStructPos + 1)
}
if (replacedStructPos > 0) {
tryToMergeWithLeft(structs, replacedStructPos)
}
}
// @todo Merge all the transactions into one and provide send the data as a single update message
// @todo implement a dedicatet event that we can use to send updates to other peer
y.emit('afterTransactionCleanup', [transaction, y])
}
y.emit('afterTransactionCleanup', [transaction, y])
y._transactionCleanups = []
}
}
}

View File

@@ -39,6 +39,11 @@ export class Y extends Observable {
* @private
*/
this._transaction = null
/**
* @type {Array<Transaction>}
* @private
*/
this._transactionCleanups = []
}
/**
* Changes that happen inside of a transaction are bundled. This means that
@@ -47,11 +52,12 @@ export class Y extends Observable {
* other peers.
*
* @param {function(Transaction):void} f The function that should be executed as a transaction
* @param {any} [origin] Origin of who started the transaction. Will be stored on transaction.origin
*
* @public
*/
transact (f) {
transact(this, f)
transact (f, origin = null) {
transact(this, f, origin)
}
/**
* Define a shared data type.

View File

@@ -12,6 +12,7 @@ import {
getState,
findRootTypeKey,
AbstractItem,
ItemType,
ID, StructStore, Y, AbstractType // eslint-disable-line
} from '../internals.js'
@@ -240,7 +241,17 @@ export const createAbsolutePositionFromCursor = (cursor, y) => {
if (tname !== null) {
type = y.get(tname)
} else if (typeID !== null) {
type = getItemType(store, typeID).type
if (getState(store, typeID.client) <= typeID.clock) {
// type does not exist yet
return null
}
const struct = getItemType(store, typeID)
if (struct instanceof ItemType) {
type = struct.type
} else {
// struct is garbage collected
return null
}
} else {
throw error.unexpectedCase()
}
@@ -259,8 +270,5 @@ export const createAbsolutePositionFromCursor = (cursor, y) => {
* @function
*/
export const compareCursors = (a, b) => a === b || (
a !== null && b !== null && a.tname === b.tname && (
(a.item !== null && b.item !== null && compareIDs(a.item, b.item)) ||
(a.type !== null && b.type !== null && compareIDs(a.type, b.type))
)
a !== null && b !== null && a.tname === b.tname && compareIDs(a.item, b.item) && compareIDs(a.type, b.type)
)

View File

@@ -9,24 +9,23 @@ import {
import * as t from 'lib0/testing.js'
import * as prng from 'lib0/prng.js'
import { createMutex } from 'lib0/mutex.js'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
import * as syncProtocol from 'y-protocols/sync.js'
/**
* @param {TestYInstance} y
* @param {Y.Transaction} transaction
* @param {TestYInstance} y
*/
const afterTransaction = (y, transaction) => {
y.mMux(() => {
const afterTransaction = (transaction, y) => {
if (transaction.origin !== y.tc) {
const m = transaction.updateMessage
if (m !== null) {
const encoder = encoding.createEncoder()
syncProtocol.writeUpdate(encoder, m)
broadcastMessage(y, encoding.toBuffer(encoder))
}
})
}
}
/**
@@ -59,11 +58,6 @@ export class TestYInstance extends Y.Y {
* @type {Map<TestYInstance, Array<ArrayBuffer>>}
*/
this.receiving = new Map()
/**
* Message mutex
* @type {Function}
*/
this.mMux = createMutex()
testConnector.allConns.add(this)
// set up observe on local model
this.on('afterTransactionCleanup', afterTransaction)
@@ -165,11 +159,9 @@ export class TestConnector {
return this.flushRandomMessage()
}
const encoder = encoding.createEncoder()
receiver.mMux(() => {
// console.log('receive (' + sender.userID + '->' + receiver.userID + '):\n', syncProtocol.stringifySyncMessage(decoding.createDecoder(m), receiver))
// do not publish data created when this function is executed (could be ss2 or update message)
syncProtocol.readSyncMessage(decoding.createDecoder(m), encoder, receiver)
})
// console.log('receive (' + sender.userID + '->' + receiver.userID + '):\n', syncProtocol.stringifySyncMessage(decoding.createDecoder(m), receiver))
// do not publish data created when this function is executed (could be ss2 or update message)
syncProtocol.readSyncMessage(decoding.createDecoder(m), encoder, receiver, receiver.tc)
if (encoding.length(encoder) > 0) {
// send reply message
sender._receive(encoding.toBuffer(encoder), receiver)
@@ -230,11 +222,13 @@ export class TestConnector {
}
/**
* @template T
* @param {t.TestCase} tc
* @param {{users?:number}} conf
* @return {{testConnector:TestConnector,users:Array<TestYInstance>,array0:Y.Array<any>,array1:Y.Array<any>,array2:Y.Array<any>,map0:Y.Map<any>,map1:Y.Map<any>,map2:Y.Map<any>,map3:Y.Map<any>,text0:Y.Text,text1:Y.Text,text2:Y.Text,xml0:Y.XmlElement,xml1:Y.XmlElement,xml2:Y.XmlElement}}
* @param {InitTestObjectCallback<T>} [initTestObject]
* @return {{testObjects:Array<any>,testConnector:TestConnector,users:Array<TestYInstance>,array0:Y.Array<any>,array1:Y.Array<any>,array2:Y.Array<any>,map0:Y.Map<any>,map1:Y.Map<any>,map2:Y.Map<any>,map3:Y.Map<any>,text0:Y.Text,text1:Y.Text,text2:Y.Text,xml0:Y.XmlElement,xml1:Y.XmlElement,xml2:Y.XmlElement}}
*/
export const init = (tc, { users = 5 } = {}) => {
export const init = (tc, { users = 5 } = {}, initTestObject) => {
/**
* @type {Object<string,any>}
*/
@@ -254,6 +248,7 @@ export const init = (tc, { users = 5 } = {}) => {
result['text' + i] = y.get('text', Y.Text)
}
testConnector.syncAll()
result.testObjects = result.users.map(initTestObject || (() => null))
// @ts-ignore
return result
}
@@ -365,15 +360,24 @@ export const compareDS = (ds1, ds2) => {
}
/**
* @param {t.TestCase} tc
* @param {Array<function(TestYInstance,prng.PRNG):void>} mods
* @param {number} iterations
* @template T
* @callback InitTestObjectCallback
* @param {TestYInstance} y
* @return {T}
*/
export const applyRandomTests = (tc, mods, iterations) => {
/**
* @template T
* @param {t.TestCase} tc
* @param {Array<function(TestYInstance,prng.PRNG,T):void>} mods
* @param {number} iterations
* @param {InitTestObjectCallback<T>} [initTestObject]
*/
export const applyRandomTests = (tc, mods, iterations, initTestObject) => {
const gen = tc.prng
const result = init(tc, { users: 5 })
const result = init(tc, { users: 5 }, initTestObject || (() => null))
const { testConnector, users } = result
for (var i = 0; i < iterations; i++) {
for (let i = 0; i < iterations; i++) {
if (prng.int31(gen, 0, 100) <= 2) {
// 2% chance to disconnect/reconnect a random user
if (prng.bool(gen)) {
@@ -388,9 +392,9 @@ export const applyRandomTests = (tc, mods, iterations) => {
// 50% chance to flush a random message
testConnector.flushRandomMessage()
}
let user = prng.oneOf(gen, users)
var test = prng.oneOf(gen, mods)
test(user, gen)
const user = prng.int31(gen, 0, users.length - 1)
const test = prng.oneOf(gen, mods)
test(users[user], gen, result.testObjects[user])
}
compare(users)
return result

View File

@@ -144,6 +144,32 @@ export const testInsertAndDeleteEvents = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testNestedObserverEvents = tc => {
const { array0, users } = init(tc, { users: 2 })
/**
* @type {Array<number>}
*/
const vals = []
array0.observe(e => {
if (array0.length === 1) {
// inserting, will call this observer again
// we expect that this observer is called after this event handler finishedn
array0.insert(1, [1])
vals.push(0)
} else {
// this should be called the second time an element is inserted (above case)
vals.push(1)
}
})
array0.insert(0, [0])
t.compareArrays(vals, [0, 1])
t.compareArrays(array0.toArray(), [0, 1])
compare(users)
}
/**
* @param {t.TestCase} tc
*/
@@ -309,8 +335,8 @@ const arrayTransactions = [
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYarrayTests20 = tc => {
applyRandomTests(tc, arrayTransactions, 3)
export const testRepeatGeneratingYarrayTests4 = tc => {
applyRandomTests(tc, arrayTransactions, 4)
}
/**

View File

@@ -54,9 +54,9 @@
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"maxNodeModuleJsDepth": 5,
"maxNodeModuleJsDepth": 0,
// "types": ["./src/utils/typedefs.js"]
},
"include": ["./src/**/*", "./tests/**/*"],
"exclude": ["../lib0/**/*", "node_modules"]
"exclude": ["../lib0/**/*", "node_modules/**/*", "dist", "dist/**/*.js"]
}