From c92f9874966f25c9322e3a97bbdc4e570a3e1120 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Sun, 22 Oct 2017 19:12:50 +0200 Subject: [PATCH] fix some tests, implement event classes for types, and re-implement logging --- rollup.test.js | 2 +- src/Connector.js | 45 +++--- src/MessageHandler/integrateRemoteStructs.js | 19 ++- src/MessageHandler/messageToString.js | 6 +- src/MessageHandler/syncStep2.js | 5 +- src/MessageHandler/update.js | 19 --- src/Store/OperationStore.js | 4 +- src/Struct/Item.js | 13 +- src/Struct/Type.js | 4 +- src/Type/YArray.js | 49 ++++-- src/Type/YMap.js | 30 +++- src/Type/y-xml/YXmlFragment.js | 19 +-- src/Type/y-xml/utils.js | 14 +- src/Y.js | 31 ++-- test/y-array.tests.js | 148 ++++++------------- tests-lib/helper.js | 39 +++-- tests-lib/test-connector.js | 47 +++--- 17 files changed, 235 insertions(+), 259 deletions(-) delete mode 100644 src/MessageHandler/update.js diff --git a/rollup.test.js b/rollup.test.js index c74129e6..dca3e47d 100644 --- a/rollup.test.js +++ b/rollup.test.js @@ -3,7 +3,7 @@ import commonjs from 'rollup-plugin-commonjs' import multiEntry from 'rollup-plugin-multi-entry' export default { - entry: 'test/y-xml.tests.js', + entry: 'test/y-array.tests.js', moduleName: 'y-tests', format: 'umd', plugins: [ diff --git a/src/Connector.js b/src/Connector.js index 5ee396c8..3faee8b8 100644 --- a/src/Connector.js +++ b/src/Connector.js @@ -3,7 +3,7 @@ import BinaryDecoder from './Binary/Decoder.js' import { sendSyncStep1, readSyncStep1 } from './MessageHandler/syncStep1.js' import { readSyncStep2 } from './MessageHandler/syncStep2.js' -import { readUpdate } from './MessageHandler/update.js' +import { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.js' import debug from 'debug' @@ -136,19 +136,21 @@ export default class AbstractConnector { } send (uid, buffer) { + const y = this.y if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) { throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - don\'t use this method to send custom messages') } - this.log('%s: Send \'%y\' to %s', this.y.userID, buffer, uid) - this.logMessage('Message: %Y', buffer) + this.log('%s: Send \'%y\' to %s', y.userID, buffer, uid) + this.logMessage('Message: %Y', [y, buffer]) } broadcast (buffer) { + const y = this.y if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) { throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - don\'t use this method to send custom messages') } - this.log('%s: Broadcast \'%y\'', this.y.userID, buffer) - this.logMessage('Message: %Y', buffer) + this.log('%s: Broadcast \'%y\'', y.userID, buffer) + this.logMessage('Message: %Y', [y, buffer]) } /* @@ -177,7 +179,7 @@ export default class AbstractConnector { this.broadcast(this.broadcastBuffer.createBuffer()) this.broadcastBuffer = new BinaryEncoder() } - }) + }, 0) } } @@ -199,11 +201,13 @@ export default class AbstractConnector { You received a raw message, and you know that it is intended for Yjs. Then call this function. */ receiveMessage (sender, buffer, skipAuth) { + const y = this.y + const userID = y.userID skipAuth = skipAuth || false if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) { return Promise.reject(new Error('Expected Message to be an ArrayBuffer or Uint8Array!')) } - if (sender === this.y.userID) { + if (sender === userID) { return Promise.resolve() } let decoder = new BinaryDecoder(buffer) @@ -212,8 +216,8 @@ export default class AbstractConnector { encoder.writeVarString(roomname) let messageType = decoder.readVarString() let senderConn = this.connections.get(sender) - this.log('%s: Receive \'%s\' from %s', this.y.userID, messageType, sender) - this.logMessage('Message: %Y', buffer) + this.log('%s: Receive \'%s\' from %s', userID, messageType, sender) + this.logMessage('Message: %Y', [y, buffer]) if (senderConn == null && !skipAuth) { throw new Error('Received message from unknown peer!') } @@ -222,10 +226,10 @@ export default class AbstractConnector { if (senderConn.auth == null) { senderConn.processAfterAuth.push([messageType, senderConn, decoder, encoder, sender]) // check auth - return this.checkAuth(auth, this.y, sender).then(authPermissions => { + return this.checkAuth(auth, y, sender).then(authPermissions => { if (senderConn.auth == null) { senderConn.auth = authPermissions - this.y.emit('userAuthenticated', { + y.emit('userAuthenticated', { user: senderConn.uid, auth: authPermissions }) @@ -250,16 +254,17 @@ export default class AbstractConnector { if (messageType === 'sync step 1' && (senderConn.auth === 'write' || senderConn.auth === 'read')) { // cannot wait for sync step 1 to finish, because we may wait for sync step 2 in sync step 1 (->lock) readSyncStep1(decoder, encoder, this.y, senderConn, sender) - } else if (messageType === 'sync step 2' && senderConn.auth === 'write') { - this.y.transact(() => { - readSyncStep2(decoder, encoder, this.y, senderConn, sender) - }) - } else if (messageType === 'update' && (skipAuth || senderConn.auth === 'write')) { - this.y.transact(() => { - readUpdate(decoder, encoder, this.y, senderConn, sender) - }) } else { - throw new Error('Unable to receive message') + const y = this.y + y.transact(function () { + if (messageType === 'sync step 2' && senderConn.auth === 'write') { + readSyncStep2(decoder, encoder, y, senderConn, sender) + } else if (messageType === 'update' && (skipAuth || senderConn.auth === 'write')) { + integrateRemoteStructs(decoder, encoder, y, senderConn, sender) + } else { + throw new Error('Unable to receive message') + } + }, true) } } diff --git a/src/MessageHandler/integrateRemoteStructs.js b/src/MessageHandler/integrateRemoteStructs.js index 8bc2ba14..d3246cd6 100644 --- a/src/MessageHandler/integrateRemoteStructs.js +++ b/src/MessageHandler/integrateRemoteStructs.js @@ -28,9 +28,7 @@ function _integrateRemoteStructHelper (y, struct) { missingDef.missing-- if (missingDef.missing === 0) { let missing = missingDef.struct._fromBinary(y, missingDef.decoder) - if (missing.length > 0) { - console.error('Missing should be empty!') - } else { + if (missing.length === 0) { y._readyToIntegrate.push(missingDef.struct) } } @@ -42,6 +40,21 @@ function _integrateRemoteStructHelper (y, struct) { } } +export function stringifyStructs (y, decoder, strBuilder) { + while (decoder.length !== decoder.pos) { + 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.map(id => `ID (user: ${id.user}, clock: ${id.clock})`).join(', ') + } + logMessage += '\n' + strBuilder.push(logMessage) + } +} + export function integrateRemoteStructs (decoder, encoder, y) { while (decoder.length !== decoder.pos) { let decoderPos = decoder.pos diff --git a/src/MessageHandler/messageToString.js b/src/MessageHandler/messageToString.js index ca8dadf4..fa4ff5a3 100644 --- a/src/MessageHandler/messageToString.js +++ b/src/MessageHandler/messageToString.js @@ -1,16 +1,16 @@ import BinaryDecoder from '../Binary/Decoder.js' -import { stringifyUpdate } from './update.js' +import { stringifyStructs } from './integrateRemoteStructs.js' import { stringifySyncStep1 } from './syncStep1.js' import { stringifySyncStep2 } from './syncStep2.js' -export function messageToString (y, buffer) { +export function messageToString ([y, buffer]) { let decoder = new BinaryDecoder(buffer) decoder.readVarString() // read roomname let type = decoder.readVarString() let strBuilder = [] strBuilder.push('\n === ' + type + ' ===\n') if (type === 'update') { - stringifyUpdate(y, decoder, strBuilder) + stringifyStructs(y, decoder, strBuilder) } else if (type === 'sync step 1') { stringifySyncStep1(y, decoder, strBuilder) } else if (type === 'sync step 2') { diff --git a/src/MessageHandler/syncStep2.js b/src/MessageHandler/syncStep2.js index 387e54d1..713cf243 100644 --- a/src/MessageHandler/syncStep2.js +++ b/src/MessageHandler/syncStep2.js @@ -1,11 +1,9 @@ -import { integrateRemoteStructs } from './integrateRemoteStructs.js' -import { stringifyUpdate } from './update.js' +import { stringifyStructs, integrateRemoteStructs } from './integrateRemoteStructs.js' import { readDeleteSet } from './deleteSet.js' export function stringifySyncStep2 (y, decoder, strBuilder) { strBuilder.push(' - auth: ' + decoder.readVarString() + '\n') strBuilder.push(' == OS: \n') - stringifyUpdate(y, decoder, strBuilder) // write DS to string strBuilder.push(' == DS: \n') let len = decoder.readUint32() @@ -20,6 +18,7 @@ export function stringifySyncStep2 (y, decoder, strBuilder) { strBuilder.push(`[${from}, ${to}, ${gc}]`) } } + stringifyStructs(y, decoder, strBuilder) } export function readSyncStep2 (decoder, encoder, y, senderConn, sender) { diff --git a/src/MessageHandler/update.js b/src/MessageHandler/update.js deleted file mode 100644 index 198c85ed..00000000 --- a/src/MessageHandler/update.js +++ /dev/null @@ -1,19 +0,0 @@ - -import { getStruct } from '../Util/structReferences.js' - -export function stringifyUpdate (y, decoder, strBuilder) { - while (decoder.length !== decoder.pos) { - 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.map(m => m._logString()).join(', ') - } - logMessage += '\n' - strBuilder.push(logMessage) - } -} - -export { integrateRemoteStructs as readUpdate } from './integrateRemoteStructs.js' diff --git a/src/Store/OperationStore.js b/src/Store/OperationStore.js index 4587d664..73daf212 100644 --- a/src/Store/OperationStore.js +++ b/src/Store/OperationStore.js @@ -15,7 +15,9 @@ export default class OperationStore extends Tree { struct = new Constr() struct._id = id struct._parent = y - struct._integrate(y) + y.transact(() => { + struct._integrate(y) + }) this.put(struct) } return struct diff --git a/src/Struct/Item.js b/src/Struct/Item.js index 69da21d2..7b9d81d3 100644 --- a/src/Struct/Item.js +++ b/src/Struct/Item.js @@ -2,6 +2,7 @@ import { getReference } from '../Util/structReferences.js' import ID from '../Util/ID.js' import { RootFakeUserID } from '../Util/RootID.js' import Delete from './Delete.js' +import { transactionTypeChanged } from '../Transaction.js' /** * Helper utility to split an Item (see _splitAt) @@ -64,10 +65,7 @@ export default class Item { del._length = this._length del._integrate(y, true) } - const parent = this._parent - if (parent !== y && !parent._deleted) { - y._transactionChangedTypes.set(parent, this._parentSub) - } + transactionTypeChanged(y, this._parent, this._parentSub) } /** * This is called right before this struct receives any children. @@ -98,7 +96,7 @@ export default class Item { // missing content from user throw new Error('Can not apply yet!') } - if (!parent._deleted && !y._transactionChangedTypes.has(parent) && !y._transactionNewTypes.has(parent)) { + 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() @@ -178,10 +176,7 @@ export default class Item { } } y.os.put(this) - if (parent !== y && !parent._deleted) { - y._transactionChangedTypes.set(parent, parentSub) - } - + transactionTypeChanged(y, parent, parentSub) if (this._id.user !== RootFakeUserID) { if (y.connector._forwardAppliedStructs || this._id.user === y.userID) { y.connector.broadcastStruct(this) diff --git a/src/Struct/Type.js b/src/Struct/Type.js index 21ffd3f4..5588d984 100644 --- a/src/Struct/Type.js +++ b/src/Struct/Type.js @@ -29,7 +29,7 @@ export default class Type extends Item { this._eventHandler.removeEventListener(f) } _integrate (y) { - y._transactionNewTypes.add(this) + y._transaction.newTypes.add(this) super._integrate(y) this._y = y // when integrating children we must make sure to @@ -48,7 +48,7 @@ export default class Type extends Item { } _delete (y, createDelete) { super._delete(y, createDelete) - y._transactionChangedTypes.delete(this) + y._transaction.changedTypes.delete(this) // delete map types for (let value of this._map.values()) { if (value instanceof Item && !value._deleted) { diff --git a/src/Type/YArray.js b/src/Type/YArray.js index 2649c3a8..f4a4c3c8 100644 --- a/src/Type/YArray.js +++ b/src/Type/YArray.js @@ -1,9 +1,16 @@ import Type from '../Struct/Type.js' import ItemJSON from '../Struct/ItemJSON.js' +class YArrayEvent { + constructor (yarray, remote) { + this.target = yarray + this.remote = remote + } +} + export default class YArray extends Type { - _callObserver () { - this._eventHandler.callEventListeners({}) + _callObserver (parentSubs, remote) { + this._eventHandler.callEventListeners(new YArrayEvent(this, remote)) } get (i) { // TODO: This can be improved! @@ -107,12 +114,13 @@ export default class YArray extends Type { } item = item._right } - if (length > 0) { - throw new Error('Delete exceeds the range of the YArray') - } }) + if (length > 0) { + throw new Error('Delete exceeds the range of the YArray') + } } insertAfter (left, content) { + const y = this._y const apply = () => { let right if (left === null) { @@ -123,10 +131,13 @@ export default class YArray extends Type { let prevJsonIns = null for (let i = 0; i < content.length; i++) { let c = content[i] + if (typeof c === 'function') { + c = new c() // eslint-disable-line new-cap + } if (c instanceof Type) { if (prevJsonIns !== null) { - if (this._y !== null) { - prevJsonIns._integrate(this._y) + if (y !== null) { + prevJsonIns._integrate(y) } left = prevJsonIns prevJsonIns = null @@ -136,8 +147,8 @@ export default class YArray extends Type { c._right = right c._right_origin = right c._parent = this - if (this._y !== null) { - c._integrate(this._y) + if (y !== null) { + c._integrate(y) } else if (left === null) { this._start = c } @@ -155,12 +166,12 @@ export default class YArray extends Type { prevJsonIns._content.push(c) } } - if (prevJsonIns !== null && this._y !== null) { - prevJsonIns._integrate(this._y) + if (prevJsonIns !== null && y !== null) { + prevJsonIns._integrate(y) } } - if (this._y !== null) { - this._y.transact(apply) + if (y !== null) { + y.transact(apply) } else { apply() } @@ -170,13 +181,19 @@ export default class YArray extends Type { let left = null let right = this._start let count = 0 + const y = this._y while (right !== null) { - if (count <= pos && pos < count + right._length) { - right = right._splitAt(this._y, pos - count) + const rightLen = right._deleted ? 0 : (right._length - 1) + if (count <= pos && pos <= count + rightLen) { + const splitDiff = pos - count + right = right._splitAt(y, splitDiff) left = right._left + count += splitDiff break } - count += right._length + if (!right._deleted) { + count += right._length + } left = right right = right._right } diff --git a/src/Type/YMap.js b/src/Type/YMap.js index 3ed44973..76ae00f0 100644 --- a/src/Type/YMap.js +++ b/src/Type/YMap.js @@ -2,11 +2,17 @@ import Type from '../Struct/Type.js' import Item from '../Struct/Item.js' import ItemJSON from '../Struct/ItemJSON.js' +class YMapEvent { + constructor (ymap, subs, remote) { + this.target = ymap + this.keysChanged = subs + this.remote = remote + } +} + export default class YMap extends Type { - _callObserver (parentSub) { - this._eventHandler.callEventListeners({ - name: parentSub - }) + _callObserver (parentSubs, remote) { + this._eventHandler.callEventListeners(new YMapEvent(this, parentSubs, remote)) } toJSON () { const map = {} @@ -36,13 +42,21 @@ export default class YMap extends Type { }) } set (key, value) { - this._y.transact(() => { + const y = this._y + y.transact(() => { const old = this._map.get(key) || null if (old !== null) { - old._delete(this._y) + if (old instanceof ItemJSON && old._content[0] === value) { + // Trying to overwrite with same value + // break here + return value + } + old._delete(y) } let v - if (value instanceof Item) { + if (typeof value === 'function') { + v = new value() // eslint-disable-line new-cap + } else if (value instanceof Item) { v = value } else { v = new ItemJSON() @@ -52,7 +66,7 @@ export default class YMap extends Type { v._right_origin = old v._parent = this v._parentSub = key - v._integrate(this._y) + v._integrate(y) }) return value } diff --git a/src/Type/y-xml/YXmlFragment.js b/src/Type/y-xml/YXmlFragment.js index 0ca986f5..8366f122 100644 --- a/src/Type/y-xml/YXmlFragment.js +++ b/src/Type/y-xml/YXmlFragment.js @@ -4,6 +4,7 @@ import { defaultDomFilter, applyChangesFromDom, reflectChangesOnDom } from './ut import YArray from '../YArray.js' import YXmlText from './YXmlText.js' +import YXmlEvent from './YXmlEvent.js' function domToYXml (parent, doms) { const types = [] @@ -65,22 +66,8 @@ export default class YXmlFragment extends YArray { xml.setDomFilter(f) }) } - _callObserver (parentSub) { - let event - if (parentSub !== null) { - event = { - type: 'attributeChanged', - name: parentSub, - value: this.getAttribute(parentSub), - target: this - } - } else { - event = { - type: 'contentChanged', - target: this - } - } - this._eventHandler.callEventListeners(event) + _callObserver (parentSubs, remote) { + this._eventHandler.callEventListeners(new YXmlEvent(this, parentSubs, remote)) } toString () { return this.map(xml => xml.toString()).join('') diff --git a/src/Type/y-xml/utils.js b/src/Type/y-xml/utils.js index 70e40008..4b86ac26 100644 --- a/src/Type/y-xml/utils.js +++ b/src/Type/y-xml/utils.js @@ -131,13 +131,17 @@ export function reflectChangesOnDom (event) { yxml._mutualExclude(() => { // TODO: do this once before applying stuff // let anchorViewPosition = getAnchorViewPosition(yxml._scrollElement) - if (event.type === 'attributeChanged') { - if (event.value === undefined) { - dom.removeAttribute(event.name) + + // update attributes + event.attributesChanged.forEach(attributeName => { + const value = yxml.getAttribute(attributeName) + if (value === undefined) { + dom.remoteAttribute(attributeName) } else { - dom.setAttribute(event.name, event.value) + dom.setAttribute(attributeName, value) } - } else if (event.type === 'contentChanged') { + }) + if (event.childListChanged) { // create fragment of undeleted nodes const fragment = document.createDocumentFragment() yxml.forEach(function (t) { diff --git a/src/Y.js b/src/Y.js index fe183e0e..7bdc0482 100644 --- a/src/Y.js +++ b/src/Y.js @@ -16,12 +16,7 @@ import { YXmlFragment, YXmlElement, YXmlText } from './Type/y-xml/y-xml.js' import BinaryDecoder from './Binary/Decoder.js' import debug from 'debug' - -function callTypesAfterTransaction (y) { - y._transactionChangedTypes.forEach(function (parentSub, type) { - type._callObserver(parentSub) - }) -} +import Transaction from './Transaction.js' export default class Y extends NamedEventHandler { constructor (opts) { @@ -42,25 +37,27 @@ export default class Y extends NamedEventHandler { this._missingStructs = new Map() this._readyToIntegrate = [] this._transactionsInProgress = 0 - // types added during transaction - this._transactionNewTypes = new Set() - // changed types (does not include new types) - this._transactionChangedTypes = new Map() - this.on('afterTransaction', callTypesAfterTransaction) + this._transaction = null } _beforeChange () {} - transact (f) { - this._transactionsInProgress++ + transact (f, remote = false) { + let initialCall = this._transaction === null + if (initialCall) { + this._transaction = new Transaction(this) + } try { f() } catch (e) { console.error(e) } - this._transactionsInProgress-- - if (this._transactionsInProgress === 0) { + if (initialCall) { + // emit change events on changed types + this._transaction.changedTypes.forEach(function (subs, type) { + type._callObserver(subs, remote) + }) + this._transaction = null + // when all changes & events are processed, emit afterTransaction event this.emit('afterTransaction', this) - this._transactionChangedTypes = new Map() - this._transactionNewTypes = new Set() } } // fake _start for root properties (y.set('name', type)) diff --git a/test/y-array.tests.js b/test/y-array.tests.js index 1e1d48f6..ec285913 100644 --- a/test/y-array.tests.js +++ b/test/y-array.tests.js @@ -125,24 +125,15 @@ test('insert & delete events', async function array8 (t) { }) array0.insert(0, [0, 1, 2]) compareEvent(t, event, { - type: 'insert', - index: 0, - values: [0, 1, 2], - length: 3 + remote: false }) array0.delete(0) compareEvent(t, event, { - type: 'delete', - index: 0, - length: 1, - values: [0] + remote: false }) array0.delete(0, 2) compareEvent(t, event, { - type: 'delete', - index: 0, - length: 2, - values: [1, 2] + remote: false }) await compareUsers(t, users) }) @@ -155,19 +146,11 @@ test('insert & delete events for types', async function array9 (t) { }) array0.insert(0, [Y.Array]) compareEvent(t, event, { - type: 'insert', - object: array0, - index: 0, - length: 1 + remote: false }) - var type = array0.get(0) - t.assert(type._model != null, 'Model of type is defined') array0.delete(0) compareEvent(t, event, { - type: 'delete', - object: array0, - index: 0, - length: 1 + remote: false }) await compareUsers(t, users) }) @@ -180,31 +163,19 @@ test('insert & delete events for types (2)', async function array10 (t) { }) array0.insert(0, ['hi', Y.Map]) compareEvent(t, events[0], { - type: 'insert', - object: array0, - index: 0, - length: 1, - values: ['hi'] - }) - compareEvent(t, events[1], { - type: 'insert', - object: array0, - index: 1, - length: 1 + remote: false }) + t.assert(events.length === 1, 'Event is triggered exactly once for insertion of two elements') array0.delete(1) - compareEvent(t, events[2], { - type: 'delete', - object: array0, - index: 1, - length: 1 + compareEvent(t, events[1], { + remote: false }) + t.assert(events.length === 2, 'Event is triggered exactly once for deletion') await compareUsers(t, users) }) test('garbage collector', async function gc1 (t) { var { users, array0 } = await initArrays(t, { users: 3 }) - array0.insert(0, ['x', 'y', 'z']) await flushAll(t, users) users[0].disconnect() @@ -215,60 +186,29 @@ test('garbage collector', async function gc1 (t) { await compareUsers(t, users) }) -test('event has correct value when setting a primitive on a YArray (same user)', async function array11 (t) { - var { array0, users } = await initArrays(t, { users: 3 }) - +test('event target is set correctly (local)', async function array11 (t) { + let { array0, users } = await initArrays(t, { users: 3 }) var event array0.observe(function (e) { event = e }) array0.insert(0, ['stuff']) - t.assert(event.values[0] === event.object.get(0), 'compare value with get method') - t.assert(event.values[0] === 'stuff', 'check that value is actually present') - t.assert(event.values[0] === array0.toJSON()[0], '.toJSON works as expected') + t.assert(event.target === array0, '"target" property is set correctly') await compareUsers(t, users) }) -test('event has correct value when setting a primitive on a YArray (received from another user)', async function array12 (t) { - var { users, array0, array1 } = await initArrays(t, { users: 3 }) - +test('event target is set correctly (remote user)', async function array12 (t) { + let { array0, array1, users } = await initArrays(t, { users: 3 }) var event array0.observe(function (e) { event = e }) array1.insert(0, ['stuff']) await flushAll(t, users) - t.assert(event.values[0] === event.object.get(0), 'compare value with get method') - t.assert(event.values[0] === 'stuff', 'check that value is actually present') - t.assert(event.values[0] === array0.toJSON()[0], '.toJSON works as expected') - await compareUsers(t, users) -}) - -test('event has correct value when setting a type on a YArray (same user)', async function array13 (t) { - var { array0, users } = await initArrays(t, { users: 3 }) - - var event - array0.observe(function (e) { - event = e + compareEvent(t, event, { + remote: true }) - array0.insert(0, [Y.Array]) - t.assert(event.values[0] === event.object.get(0), 'compare value with get method') - t.assert(event.values[0] != null, 'event.value exists') - t.assert(event.values[0] === array0.toJSON()[0], '.toJSON works as expected') - await compareUsers(t, users) -}) -test('event has correct value when setting a type on a YArray (ops received from another user)', async function array14 (t) { - var { users, array0, array1 } = await initArrays(t, { users: 3 }) - - var event - array0.observe(function (e) { - event = e - }) - array1.insert(0, [Y.Array]) - await flushAll(t, users) - t.assert(event.values[0] === event.object.get(0), 'compare value with get method') - t.assert(event.values[0] != null, 'event.value exists') - t.assert(event.values[0] === array0.toJSON()[0], '.toJSON works as expected') + t.assert(event.target === array0, '"target" property is set correctly') await compareUsers(t, users) }) @@ -279,56 +219,62 @@ function getUniqueNumber () { var arrayTransactions = [ function insert (t, user, chance) { + const yarray = user.get('array', Y.Array) var uniqueNumber = getUniqueNumber() var content = [] var len = chance.integer({ min: 1, max: 4 }) for (var i = 0; i < len; i++) { content.push(uniqueNumber) } - var pos = chance.integer({ min: 0, max: user.share.array.length }) - user.share.array.insert(pos, content) + var pos = chance.integer({ min: 0, max: yarray.length }) + yarray.insert(pos, content) }, + /* function insertTypeArray (t, user, chance) { - var pos = chance.integer({ min: 0, max: user.share.array.length }) - user.share.array.insert(pos, [Y.Array]) - var array2 = user.share.array.get(pos) + const yarray = user.get('array', Y.Array) + var pos = chance.integer({ min: 0, max: yarray.length }) + yarray.insert(pos, [Y.Array]) + var array2 = yarray.get(pos) array2.insert(0, [1, 2, 3, 4]) }, function insertTypeMap (t, user, chance) { - var pos = chance.integer({ min: 0, max: user.share.array.length }) - user.share.array.insert(pos, [Y.Map]) - var map = user.share.array.get(pos) + const yarray = user.get('array', Y.Array) + var pos = chance.integer({ min: 0, max: yarray.length }) + yarray.insert(pos, [Y.Map]) + var map = yarray.get(pos) map.set('someprop', 42) map.set('someprop', 43) map.set('someprop', 44) }, function _delete (t, user, chance) { - var length = user.share.array._content.length + const yarray = user.get('array', Y.Array) + var length = yarray.length if (length > 0) { - var pos = chance.integer({ min: 0, max: length - 1 }) - var delLength = chance.integer({ min: 1, max: Math.min(2, length - pos) }) - if (user.share.array._content[pos].type != null) { + var somePos = chance.integer({ min: 0, max: length - 1 }) + var delLength = chance.integer({ min: 1, max: Math.min(2, length - somePos) }) + if (yarray instanceof Y.Array) { if (chance.bool()) { - var type = user.share.array.get(pos) - if (type instanceof Y.Array.typeDefinition.class) { - if (type._content.length > 0) { - pos = chance.integer({ min: 0, max: type._content.length - 1 }) - delLength = chance.integer({ min: 0, max: Math.min(2, type._content.length - pos) }) - type.delete(pos, delLength) - } - } else { - type.delete('someprop') + var type = yarray.get(somePos) + if (type.length > 0) { + somePos = chance.integer({ min: 0, max: type.length - 1 }) + delLength = chance.integer({ min: 0, max: Math.min(2, type.length - somePos) }) + type.delete(somePos, delLength) } } else { - user.share.array.delete(pos, delLength) + yarray.delete(somePos, delLength) } } else { - user.share.array.delete(pos, delLength) + yarray.delete(somePos, delLength) } } } + */ ] +test('y-array: Random tests (5)', async function randomArray5 (t) { + await applyRandomTests(t, arrayTransactions, 5) +}) + test('y-array: Random tests (42)', async function randomArray42 (t) { await applyRandomTests(t, arrayTransactions, 42) }) diff --git a/tests-lib/helper.js b/tests-lib/helper.js index 8d148ec0..ebf7fe9a 100644 --- a/tests-lib/helper.js +++ b/tests-lib/helper.js @@ -5,6 +5,7 @@ import yTest from './test-connector.js' import Chance from 'chance' import ItemJSON from '../src/Struct/ItemJSON.js' import ItemString from '../src/Struct/ItemString.js' +import { defragmentItemContent } from '../src/Util/defragmentItemContent.js' export const Y = _Y @@ -86,8 +87,12 @@ export async function compareUsers (t, users) { await flushAll(t, users) await wait() await flushAll(t, users) + await wait() + await flushAll(t, users) + await wait() + await flushAll(t, users) - var userArrayValues = users.map(u => u.get('array', Y.Array).toJSON()) + var userArrayValues = users.map(u => u.get('array', Y.Array).toJSON().map(val => JSON.stringify(val))) var userMapValues = users.map(u => u.get('map', Y.Map).toJSON()) var userXmlValues = users.map(u => u.get('xml', Y.Xml).toString()) @@ -110,22 +115,21 @@ export async function compareUsers (t, users) { }) )) var data = users.map(u => { + defragmentItemContent(u) var data = {} let ops = [] u.os.iterate(null, null, function (op) { - if (!op._deleted) { - const json = { - id: op._id, - left: op._left === null ? null : op._left._id, - right: op._right === null ? null : op._right._id, - length: op._length, - deleted: op._deleted - } - if (op instanceof ItemJSON || op instanceof ItemString) { - json.content = op._content - } - ops.push(json) + const json = { + id: op._id, + left: op._left === null ? null : op._left._id, + right: op._right === null ? null : op._right._id, + length: op._length, + deleted: op._deleted } + if (op instanceof ItemJSON || op instanceof ItemString) { + json.content = op._content + } + ops.push(json) }) data.os = ops data.ds = getDeleteSet(u) @@ -173,6 +177,13 @@ export async function initArrays (t, opts) { return attrs.filter(a => a !== 'hidden') } }) + y.on('afterTransaction', function () { + for (let missing of y._missingStructs.values()) { + if (Array.from(missing.values()).length > 0) { + console.error(new Error('Test check in "afterTransaction": missing should be empty!')) + } + } + }) } result.array0.delete(0, result.array0.length) if (result.users[0].connector.testRoom != null) { @@ -266,7 +277,7 @@ export async function applyRandomTests (t, mods, iterations) { // TODO: We do not gc all users as this does not work yet // await garbageCollectUsers(t, users) await flushAll(t, users) - await users[0].db.emptyGarbageCollector() + // await users[0].db.emptyGarbageCollector() await flushAll(t, users) } else if (chance.bool({likelihood: 10})) { // 20%*!prev chance to flush some operations diff --git a/tests-lib/test-connector.js b/tests-lib/test-connector.js index 17fe19f5..335d4182 100644 --- a/tests-lib/test-connector.js +++ b/tests-lib/test-connector.js @@ -1,6 +1,6 @@ /* global Y */ import { wait } from './helper' -import { messageToString, messageToRoomname } from '../src/MessageHandler/messageToString' +import { messageToString } from '../src/MessageHandler/messageToString' var rooms = {} @@ -14,8 +14,8 @@ export class TestRoom { this.users.set(userID, connector) for (let [uid, user] of this.users) { if (uid !== userID && (user.role === 'master' || connector.role === 'master')) { - connector.userJoined(uid, this.users.get(uid).role) - this.users.get(uid).userJoined(userID, connector.role) + connector.userJoined(uid, user.role) + user.userJoined(userID, connector.role) } } } @@ -38,13 +38,13 @@ export class TestRoom { } async flushAll (users) { let flushing = true - let allUserIds = Array.from(this.users.keys()) + let allUsers = Array.from(this.users.values()) if (users == null) { - users = allUserIds.map(id => this.users.get(id).y) + users = allUsers.map(user => user.y) } while (flushing) { await wait(10) - let res = await Promise.all(allUserIds.map(id => this.users.get(id)._flushAll(users))) + let res = await Promise.all(allUsers.map(user => user._flushAll(users))) flushing = res.some(status => status === 'flushing') } } @@ -83,13 +83,16 @@ export default function extendTestConnector (Y) { for (let [user, conn] of this.connections) { console.log(` ${user}:`) for (let i = 0; i < conn.buffer.length; i++) { - console.log(formatYjsMessage(conn.buffer[i])) + console.log(messageToString(conn.buffer[i])) } } } reconnect () { this.testRoom.join(this) - return super.reconnect() + super.reconnect() + return new Promise(resolve => { + this.whenSynced(resolve) + }) } send (uid, message) { super.send(uid, message) @@ -116,20 +119,22 @@ export default function extendTestConnector (Y) { }) } receiveMessage (sender, m) { - if (this.y.userID !== sender && this.connections.has(sender)) { - var buffer = this.connections.get(sender).buffer - if (buffer == null) { - buffer = this.connections.get(sender).buffer = [] + setTimeout(() => { + if (this.y.userID !== sender && this.connections.has(sender)) { + var buffer = this.connections.get(sender).buffer + if (buffer == null) { + buffer = this.connections.get(sender).buffer = [] + } + buffer.push(m) + if (this.chance.bool({likelihood: 30})) { + // flush 1/2 with 30% chance + var flushLength = Math.round(buffer.length / 2) + buffer.splice(0, flushLength).forEach(m => { + super.receiveMessage(sender, m) + }) + } } - buffer.push(m) - if (this.chance.bool({likelihood: 30})) { - // flush 1/2 with 30% chance - var flushLength = Math.round(buffer.length / 2) - buffer.splice(0, flushLength).forEach(m => { - super.receiveMessage(sender, m) - }) - } - } + }, 0) } async _flushAll (flushUsers) { if (flushUsers.some(u => u.connector.y.userID === this.y.userID)) {