fix some tests, implement event classes for types, and re-implement logging

This commit is contained in:
Kevin Jahns 2017-10-22 19:12:50 +02:00
parent 755c9eb16e
commit c92f987496
17 changed files with 235 additions and 259 deletions

View File

@ -3,7 +3,7 @@ import commonjs from 'rollup-plugin-commonjs'
import multiEntry from 'rollup-plugin-multi-entry' import multiEntry from 'rollup-plugin-multi-entry'
export default { export default {
entry: 'test/y-xml.tests.js', entry: 'test/y-array.tests.js',
moduleName: 'y-tests', moduleName: 'y-tests',
format: 'umd', format: 'umd',
plugins: [ plugins: [

View File

@ -3,7 +3,7 @@ import BinaryDecoder from './Binary/Decoder.js'
import { sendSyncStep1, readSyncStep1 } from './MessageHandler/syncStep1.js' import { sendSyncStep1, readSyncStep1 } from './MessageHandler/syncStep1.js'
import { readSyncStep2 } from './MessageHandler/syncStep2.js' import { readSyncStep2 } from './MessageHandler/syncStep2.js'
import { readUpdate } from './MessageHandler/update.js' import { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.js'
import debug from 'debug' import debug from 'debug'
@ -136,19 +136,21 @@ export default class AbstractConnector {
} }
send (uid, buffer) { send (uid, buffer) {
const y = this.y
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) { 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') 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.log('%s: Send \'%y\' to %s', y.userID, buffer, uid)
this.logMessage('Message: %Y', buffer) this.logMessage('Message: %Y', [y, buffer])
} }
broadcast (buffer) { broadcast (buffer) {
const y = this.y
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) { 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') 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.log('%s: Broadcast \'%y\'', y.userID, buffer)
this.logMessage('Message: %Y', buffer) this.logMessage('Message: %Y', [y, buffer])
} }
/* /*
@ -177,7 +179,7 @@ export default class AbstractConnector {
this.broadcast(this.broadcastBuffer.createBuffer()) this.broadcast(this.broadcastBuffer.createBuffer())
this.broadcastBuffer = new BinaryEncoder() 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. You received a raw message, and you know that it is intended for Yjs. Then call this function.
*/ */
receiveMessage (sender, buffer, skipAuth) { receiveMessage (sender, buffer, skipAuth) {
const y = this.y
const userID = y.userID
skipAuth = skipAuth || false skipAuth = skipAuth || false
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) { if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
return Promise.reject(new Error('Expected Message to be an ArrayBuffer or 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() return Promise.resolve()
} }
let decoder = new BinaryDecoder(buffer) let decoder = new BinaryDecoder(buffer)
@ -212,8 +216,8 @@ export default class AbstractConnector {
encoder.writeVarString(roomname) encoder.writeVarString(roomname)
let messageType = decoder.readVarString() let messageType = decoder.readVarString()
let senderConn = this.connections.get(sender) let senderConn = this.connections.get(sender)
this.log('%s: Receive \'%s\' from %s', this.y.userID, messageType, sender) this.log('%s: Receive \'%s\' from %s', userID, messageType, sender)
this.logMessage('Message: %Y', buffer) this.logMessage('Message: %Y', [y, buffer])
if (senderConn == null && !skipAuth) { if (senderConn == null && !skipAuth) {
throw new Error('Received message from unknown peer!') throw new Error('Received message from unknown peer!')
} }
@ -222,10 +226,10 @@ export default class AbstractConnector {
if (senderConn.auth == null) { if (senderConn.auth == null) {
senderConn.processAfterAuth.push([messageType, senderConn, decoder, encoder, sender]) senderConn.processAfterAuth.push([messageType, senderConn, decoder, encoder, sender])
// check auth // check auth
return this.checkAuth(auth, this.y, sender).then(authPermissions => { return this.checkAuth(auth, y, sender).then(authPermissions => {
if (senderConn.auth == null) { if (senderConn.auth == null) {
senderConn.auth = authPermissions senderConn.auth = authPermissions
this.y.emit('userAuthenticated', { y.emit('userAuthenticated', {
user: senderConn.uid, user: senderConn.uid,
auth: authPermissions auth: authPermissions
}) })
@ -250,16 +254,17 @@ export default class AbstractConnector {
if (messageType === 'sync step 1' && (senderConn.auth === 'write' || senderConn.auth === 'read')) { 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) // 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) 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 { } 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)
} }
} }

View File

@ -28,9 +28,7 @@ function _integrateRemoteStructHelper (y, struct) {
missingDef.missing-- missingDef.missing--
if (missingDef.missing === 0) { if (missingDef.missing === 0) {
let missing = missingDef.struct._fromBinary(y, missingDef.decoder) let missing = missingDef.struct._fromBinary(y, missingDef.decoder)
if (missing.length > 0) { if (missing.length === 0) {
console.error('Missing should be empty!')
} else {
y._readyToIntegrate.push(missingDef.struct) 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) { export function integrateRemoteStructs (decoder, encoder, y) {
while (decoder.length !== decoder.pos) { while (decoder.length !== decoder.pos) {
let decoderPos = decoder.pos let decoderPos = decoder.pos

View File

@ -1,16 +1,16 @@
import BinaryDecoder from '../Binary/Decoder.js' import BinaryDecoder from '../Binary/Decoder.js'
import { stringifyUpdate } from './update.js' import { stringifyStructs } from './integrateRemoteStructs.js'
import { stringifySyncStep1 } from './syncStep1.js' import { stringifySyncStep1 } from './syncStep1.js'
import { stringifySyncStep2 } from './syncStep2.js' import { stringifySyncStep2 } from './syncStep2.js'
export function messageToString (y, buffer) { export function messageToString ([y, buffer]) {
let decoder = new BinaryDecoder(buffer) let decoder = new BinaryDecoder(buffer)
decoder.readVarString() // read roomname decoder.readVarString() // read roomname
let type = decoder.readVarString() let type = decoder.readVarString()
let strBuilder = [] let strBuilder = []
strBuilder.push('\n === ' + type + ' ===\n') strBuilder.push('\n === ' + type + ' ===\n')
if (type === 'update') { if (type === 'update') {
stringifyUpdate(y, decoder, strBuilder) stringifyStructs(y, decoder, strBuilder)
} else if (type === 'sync step 1') { } else if (type === 'sync step 1') {
stringifySyncStep1(y, decoder, strBuilder) stringifySyncStep1(y, decoder, strBuilder)
} else if (type === 'sync step 2') { } else if (type === 'sync step 2') {

View File

@ -1,11 +1,9 @@
import { integrateRemoteStructs } from './integrateRemoteStructs.js' import { stringifyStructs, integrateRemoteStructs } from './integrateRemoteStructs.js'
import { stringifyUpdate } from './update.js'
import { readDeleteSet } from './deleteSet.js' import { readDeleteSet } from './deleteSet.js'
export function stringifySyncStep2 (y, decoder, strBuilder) { export function stringifySyncStep2 (y, decoder, strBuilder) {
strBuilder.push(' - auth: ' + decoder.readVarString() + '\n') strBuilder.push(' - auth: ' + decoder.readVarString() + '\n')
strBuilder.push(' == OS: \n') strBuilder.push(' == OS: \n')
stringifyUpdate(y, decoder, strBuilder)
// write DS to string // write DS to string
strBuilder.push(' == DS: \n') strBuilder.push(' == DS: \n')
let len = decoder.readUint32() let len = decoder.readUint32()
@ -20,6 +18,7 @@ export function stringifySyncStep2 (y, decoder, strBuilder) {
strBuilder.push(`[${from}, ${to}, ${gc}]`) strBuilder.push(`[${from}, ${to}, ${gc}]`)
} }
} }
stringifyStructs(y, decoder, strBuilder)
} }
export function readSyncStep2 (decoder, encoder, y, senderConn, sender) { export function readSyncStep2 (decoder, encoder, y, senderConn, sender) {

View File

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

View File

@ -15,7 +15,9 @@ export default class OperationStore extends Tree {
struct = new Constr() struct = new Constr()
struct._id = id struct._id = id
struct._parent = y struct._parent = y
struct._integrate(y) y.transact(() => {
struct._integrate(y)
})
this.put(struct) this.put(struct)
} }
return struct return struct

View File

@ -2,6 +2,7 @@ import { getReference } from '../Util/structReferences.js'
import ID from '../Util/ID.js' import ID from '../Util/ID.js'
import { RootFakeUserID } from '../Util/RootID.js' import { RootFakeUserID } from '../Util/RootID.js'
import Delete from './Delete.js' import Delete from './Delete.js'
import { transactionTypeChanged } from '../Transaction.js'
/** /**
* Helper utility to split an Item (see _splitAt) * Helper utility to split an Item (see _splitAt)
@ -64,10 +65,7 @@ export default class Item {
del._length = this._length del._length = this._length
del._integrate(y, true) del._integrate(y, true)
} }
const parent = this._parent transactionTypeChanged(y, this._parent, this._parentSub)
if (parent !== y && !parent._deleted) {
y._transactionChangedTypes.set(parent, this._parentSub)
}
} }
/** /**
* This is called right before this struct receives any children. * This is called right before this struct receives any children.
@ -98,7 +96,7 @@ export default class Item {
// missing content from user // missing content from user
throw new Error('Can not apply yet!') 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 // this is the first time parent is updated
// or this types is new // or this types is new
this._parent._beforeChange() this._parent._beforeChange()
@ -178,10 +176,7 @@ export default class Item {
} }
} }
y.os.put(this) y.os.put(this)
if (parent !== y && !parent._deleted) { transactionTypeChanged(y, parent, parentSub)
y._transactionChangedTypes.set(parent, parentSub)
}
if (this._id.user !== RootFakeUserID) { if (this._id.user !== RootFakeUserID) {
if (y.connector._forwardAppliedStructs || this._id.user === y.userID) { if (y.connector._forwardAppliedStructs || this._id.user === y.userID) {
y.connector.broadcastStruct(this) y.connector.broadcastStruct(this)

View File

@ -29,7 +29,7 @@ export default class Type extends Item {
this._eventHandler.removeEventListener(f) this._eventHandler.removeEventListener(f)
} }
_integrate (y) { _integrate (y) {
y._transactionNewTypes.add(this) y._transaction.newTypes.add(this)
super._integrate(y) super._integrate(y)
this._y = y this._y = y
// when integrating children we must make sure to // when integrating children we must make sure to
@ -48,7 +48,7 @@ export default class Type extends Item {
} }
_delete (y, createDelete) { _delete (y, createDelete) {
super._delete(y, createDelete) super._delete(y, createDelete)
y._transactionChangedTypes.delete(this) y._transaction.changedTypes.delete(this)
// delete map types // delete map types
for (let value of this._map.values()) { for (let value of this._map.values()) {
if (value instanceof Item && !value._deleted) { if (value instanceof Item && !value._deleted) {

View File

@ -1,9 +1,16 @@
import Type from '../Struct/Type.js' import Type from '../Struct/Type.js'
import ItemJSON from '../Struct/ItemJSON.js' import ItemJSON from '../Struct/ItemJSON.js'
class YArrayEvent {
constructor (yarray, remote) {
this.target = yarray
this.remote = remote
}
}
export default class YArray extends Type { export default class YArray extends Type {
_callObserver () { _callObserver (parentSubs, remote) {
this._eventHandler.callEventListeners({}) this._eventHandler.callEventListeners(new YArrayEvent(this, remote))
} }
get (i) { get (i) {
// TODO: This can be improved! // TODO: This can be improved!
@ -107,12 +114,13 @@ export default class YArray extends Type {
} }
item = item._right 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) { insertAfter (left, content) {
const y = this._y
const apply = () => { const apply = () => {
let right let right
if (left === null) { if (left === null) {
@ -123,10 +131,13 @@ export default class YArray extends Type {
let prevJsonIns = null let prevJsonIns = null
for (let i = 0; i < content.length; i++) { for (let i = 0; i < content.length; i++) {
let c = content[i] let c = content[i]
if (typeof c === 'function') {
c = new c() // eslint-disable-line new-cap
}
if (c instanceof Type) { if (c instanceof Type) {
if (prevJsonIns !== null) { if (prevJsonIns !== null) {
if (this._y !== null) { if (y !== null) {
prevJsonIns._integrate(this._y) prevJsonIns._integrate(y)
} }
left = prevJsonIns left = prevJsonIns
prevJsonIns = null prevJsonIns = null
@ -136,8 +147,8 @@ export default class YArray extends Type {
c._right = right c._right = right
c._right_origin = right c._right_origin = right
c._parent = this c._parent = this
if (this._y !== null) { if (y !== null) {
c._integrate(this._y) c._integrate(y)
} else if (left === null) { } else if (left === null) {
this._start = c this._start = c
} }
@ -155,12 +166,12 @@ export default class YArray extends Type {
prevJsonIns._content.push(c) prevJsonIns._content.push(c)
} }
} }
if (prevJsonIns !== null && this._y !== null) { if (prevJsonIns !== null && y !== null) {
prevJsonIns._integrate(this._y) prevJsonIns._integrate(y)
} }
} }
if (this._y !== null) { if (y !== null) {
this._y.transact(apply) y.transact(apply)
} else { } else {
apply() apply()
} }
@ -170,13 +181,19 @@ export default class YArray extends Type {
let left = null let left = null
let right = this._start let right = this._start
let count = 0 let count = 0
const y = this._y
while (right !== null) { while (right !== null) {
if (count <= pos && pos < count + right._length) { const rightLen = right._deleted ? 0 : (right._length - 1)
right = right._splitAt(this._y, pos - count) if (count <= pos && pos <= count + rightLen) {
const splitDiff = pos - count
right = right._splitAt(y, splitDiff)
left = right._left left = right._left
count += splitDiff
break break
} }
count += right._length if (!right._deleted) {
count += right._length
}
left = right left = right
right = right._right right = right._right
} }

View File

@ -2,11 +2,17 @@ import Type from '../Struct/Type.js'
import Item from '../Struct/Item.js' import Item from '../Struct/Item.js'
import ItemJSON from '../Struct/ItemJSON.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 { export default class YMap extends Type {
_callObserver (parentSub) { _callObserver (parentSubs, remote) {
this._eventHandler.callEventListeners({ this._eventHandler.callEventListeners(new YMapEvent(this, parentSubs, remote))
name: parentSub
})
} }
toJSON () { toJSON () {
const map = {} const map = {}
@ -36,13 +42,21 @@ export default class YMap extends Type {
}) })
} }
set (key, value) { set (key, value) {
this._y.transact(() => { const y = this._y
y.transact(() => {
const old = this._map.get(key) || null const old = this._map.get(key) || null
if (old !== 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 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 v = value
} else { } else {
v = new ItemJSON() v = new ItemJSON()
@ -52,7 +66,7 @@ export default class YMap extends Type {
v._right_origin = old v._right_origin = old
v._parent = this v._parent = this
v._parentSub = key v._parentSub = key
v._integrate(this._y) v._integrate(y)
}) })
return value return value
} }

View File

@ -4,6 +4,7 @@ import { defaultDomFilter, applyChangesFromDom, reflectChangesOnDom } from './ut
import YArray from '../YArray.js' import YArray from '../YArray.js'
import YXmlText from './YXmlText.js' import YXmlText from './YXmlText.js'
import YXmlEvent from './YXmlEvent.js'
function domToYXml (parent, doms) { function domToYXml (parent, doms) {
const types = [] const types = []
@ -65,22 +66,8 @@ export default class YXmlFragment extends YArray {
xml.setDomFilter(f) xml.setDomFilter(f)
}) })
} }
_callObserver (parentSub) { _callObserver (parentSubs, remote) {
let event this._eventHandler.callEventListeners(new YXmlEvent(this, parentSubs, remote))
if (parentSub !== null) {
event = {
type: 'attributeChanged',
name: parentSub,
value: this.getAttribute(parentSub),
target: this
}
} else {
event = {
type: 'contentChanged',
target: this
}
}
this._eventHandler.callEventListeners(event)
} }
toString () { toString () {
return this.map(xml => xml.toString()).join('') return this.map(xml => xml.toString()).join('')

View File

@ -131,13 +131,17 @@ export function reflectChangesOnDom (event) {
yxml._mutualExclude(() => { yxml._mutualExclude(() => {
// TODO: do this once before applying stuff // TODO: do this once before applying stuff
// let anchorViewPosition = getAnchorViewPosition(yxml._scrollElement) // let anchorViewPosition = getAnchorViewPosition(yxml._scrollElement)
if (event.type === 'attributeChanged') {
if (event.value === undefined) { // update attributes
dom.removeAttribute(event.name) event.attributesChanged.forEach(attributeName => {
const value = yxml.getAttribute(attributeName)
if (value === undefined) {
dom.remoteAttribute(attributeName)
} else { } else {
dom.setAttribute(event.name, event.value) dom.setAttribute(attributeName, value)
} }
} else if (event.type === 'contentChanged') { })
if (event.childListChanged) {
// create fragment of undeleted nodes // create fragment of undeleted nodes
const fragment = document.createDocumentFragment() const fragment = document.createDocumentFragment()
yxml.forEach(function (t) { yxml.forEach(function (t) {

View File

@ -16,12 +16,7 @@ import { YXmlFragment, YXmlElement, YXmlText } from './Type/y-xml/y-xml.js'
import BinaryDecoder from './Binary/Decoder.js' import BinaryDecoder from './Binary/Decoder.js'
import debug from 'debug' import debug from 'debug'
import Transaction from './Transaction.js'
function callTypesAfterTransaction (y) {
y._transactionChangedTypes.forEach(function (parentSub, type) {
type._callObserver(parentSub)
})
}
export default class Y extends NamedEventHandler { export default class Y extends NamedEventHandler {
constructor (opts) { constructor (opts) {
@ -42,25 +37,27 @@ export default class Y extends NamedEventHandler {
this._missingStructs = new Map() this._missingStructs = new Map()
this._readyToIntegrate = [] this._readyToIntegrate = []
this._transactionsInProgress = 0 this._transactionsInProgress = 0
// types added during transaction this._transaction = null
this._transactionNewTypes = new Set()
// changed types (does not include new types)
this._transactionChangedTypes = new Map()
this.on('afterTransaction', callTypesAfterTransaction)
} }
_beforeChange () {} _beforeChange () {}
transact (f) { transact (f, remote = false) {
this._transactionsInProgress++ let initialCall = this._transaction === null
if (initialCall) {
this._transaction = new Transaction(this)
}
try { try {
f() f()
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
this._transactionsInProgress-- if (initialCall) {
if (this._transactionsInProgress === 0) { // 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.emit('afterTransaction', this)
this._transactionChangedTypes = new Map()
this._transactionNewTypes = new Set()
} }
} }
// fake _start for root properties (y.set('name', type)) // fake _start for root properties (y.set('name', type))

View File

@ -125,24 +125,15 @@ test('insert & delete events', async function array8 (t) {
}) })
array0.insert(0, [0, 1, 2]) array0.insert(0, [0, 1, 2])
compareEvent(t, event, { compareEvent(t, event, {
type: 'insert', remote: false
index: 0,
values: [0, 1, 2],
length: 3
}) })
array0.delete(0) array0.delete(0)
compareEvent(t, event, { compareEvent(t, event, {
type: 'delete', remote: false
index: 0,
length: 1,
values: [0]
}) })
array0.delete(0, 2) array0.delete(0, 2)
compareEvent(t, event, { compareEvent(t, event, {
type: 'delete', remote: false
index: 0,
length: 2,
values: [1, 2]
}) })
await compareUsers(t, users) await compareUsers(t, users)
}) })
@ -155,19 +146,11 @@ test('insert & delete events for types', async function array9 (t) {
}) })
array0.insert(0, [Y.Array]) array0.insert(0, [Y.Array])
compareEvent(t, event, { compareEvent(t, event, {
type: 'insert', remote: false
object: array0,
index: 0,
length: 1
}) })
var type = array0.get(0)
t.assert(type._model != null, 'Model of type is defined')
array0.delete(0) array0.delete(0)
compareEvent(t, event, { compareEvent(t, event, {
type: 'delete', remote: false
object: array0,
index: 0,
length: 1
}) })
await compareUsers(t, users) 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]) array0.insert(0, ['hi', Y.Map])
compareEvent(t, events[0], { compareEvent(t, events[0], {
type: 'insert', remote: false
object: array0,
index: 0,
length: 1,
values: ['hi']
})
compareEvent(t, events[1], {
type: 'insert',
object: array0,
index: 1,
length: 1
}) })
t.assert(events.length === 1, 'Event is triggered exactly once for insertion of two elements')
array0.delete(1) array0.delete(1)
compareEvent(t, events[2], { compareEvent(t, events[1], {
type: 'delete', remote: false
object: array0,
index: 1,
length: 1
}) })
t.assert(events.length === 2, 'Event is triggered exactly once for deletion')
await compareUsers(t, users) await compareUsers(t, users)
}) })
test('garbage collector', async function gc1 (t) { test('garbage collector', async function gc1 (t) {
var { users, array0 } = await initArrays(t, { users: 3 }) var { users, array0 } = await initArrays(t, { users: 3 })
array0.insert(0, ['x', 'y', 'z']) array0.insert(0, ['x', 'y', 'z'])
await flushAll(t, users) await flushAll(t, users)
users[0].disconnect() users[0].disconnect()
@ -215,60 +186,29 @@ test('garbage collector', async function gc1 (t) {
await compareUsers(t, users) await compareUsers(t, users)
}) })
test('event has correct value when setting a primitive on a YArray (same user)', async function array11 (t) { test('event target is set correctly (local)', async function array11 (t) {
var { array0, users } = await initArrays(t, { users: 3 }) let { array0, users } = await initArrays(t, { users: 3 })
var event var event
array0.observe(function (e) { array0.observe(function (e) {
event = e event = e
}) })
array0.insert(0, ['stuff']) array0.insert(0, ['stuff'])
t.assert(event.values[0] === event.object.get(0), 'compare value with get method') t.assert(event.target === array0, '"target" property is set correctly')
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) await compareUsers(t, users)
}) })
test('event has correct value when setting a primitive on a YArray (received from another user)', async function array12 (t) { test('event target is set correctly (remote user)', async function array12 (t) {
var { users, array0, array1 } = await initArrays(t, { users: 3 }) let { array0, array1, users } = await initArrays(t, { users: 3 })
var event var event
array0.observe(function (e) { array0.observe(function (e) {
event = e event = e
}) })
array1.insert(0, ['stuff']) array1.insert(0, ['stuff'])
await flushAll(t, users) await flushAll(t, users)
t.assert(event.values[0] === event.object.get(0), 'compare value with get method') compareEvent(t, event, {
t.assert(event.values[0] === 'stuff', 'check that value is actually present') remote: true
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
}) })
array0.insert(0, [Y.Array]) t.assert(event.target === array0, '"target" property is set correctly')
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')
await compareUsers(t, users) await compareUsers(t, users)
}) })
@ -279,56 +219,62 @@ function getUniqueNumber () {
var arrayTransactions = [ var arrayTransactions = [
function insert (t, user, chance) { function insert (t, user, chance) {
const yarray = user.get('array', Y.Array)
var uniqueNumber = getUniqueNumber() var uniqueNumber = getUniqueNumber()
var content = [] var content = []
var len = chance.integer({ min: 1, max: 4 }) var len = chance.integer({ min: 1, max: 4 })
for (var i = 0; i < len; i++) { for (var i = 0; i < len; i++) {
content.push(uniqueNumber) content.push(uniqueNumber)
} }
var pos = chance.integer({ min: 0, max: user.share.array.length }) var pos = chance.integer({ min: 0, max: yarray.length })
user.share.array.insert(pos, content) yarray.insert(pos, content)
}, },
/*
function insertTypeArray (t, user, chance) { function insertTypeArray (t, user, chance) {
var pos = chance.integer({ min: 0, max: user.share.array.length }) const yarray = user.get('array', Y.Array)
user.share.array.insert(pos, [Y.Array]) var pos = chance.integer({ min: 0, max: yarray.length })
var array2 = user.share.array.get(pos) yarray.insert(pos, [Y.Array])
var array2 = yarray.get(pos)
array2.insert(0, [1, 2, 3, 4]) array2.insert(0, [1, 2, 3, 4])
}, },
function insertTypeMap (t, user, chance) { function insertTypeMap (t, user, chance) {
var pos = chance.integer({ min: 0, max: user.share.array.length }) const yarray = user.get('array', Y.Array)
user.share.array.insert(pos, [Y.Map]) var pos = chance.integer({ min: 0, max: yarray.length })
var map = user.share.array.get(pos) yarray.insert(pos, [Y.Map])
var map = yarray.get(pos)
map.set('someprop', 42) map.set('someprop', 42)
map.set('someprop', 43) map.set('someprop', 43)
map.set('someprop', 44) map.set('someprop', 44)
}, },
function _delete (t, user, chance) { 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) { if (length > 0) {
var pos = chance.integer({ min: 0, max: length - 1 }) var somePos = chance.integer({ min: 0, max: length - 1 })
var delLength = chance.integer({ min: 1, max: Math.min(2, length - pos) }) var delLength = chance.integer({ min: 1, max: Math.min(2, length - somePos) })
if (user.share.array._content[pos].type != null) { if (yarray instanceof Y.Array) {
if (chance.bool()) { if (chance.bool()) {
var type = user.share.array.get(pos) var type = yarray.get(somePos)
if (type instanceof Y.Array.typeDefinition.class) { if (type.length > 0) {
if (type._content.length > 0) { somePos = chance.integer({ min: 0, max: type.length - 1 })
pos = chance.integer({ min: 0, max: type._content.length - 1 }) delLength = chance.integer({ min: 0, max: Math.min(2, type.length - somePos) })
delLength = chance.integer({ min: 0, max: Math.min(2, type._content.length - pos) }) type.delete(somePos, delLength)
type.delete(pos, delLength)
}
} else {
type.delete('someprop')
} }
} else { } else {
user.share.array.delete(pos, delLength) yarray.delete(somePos, delLength)
} }
} else { } 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) { test('y-array: Random tests (42)', async function randomArray42 (t) {
await applyRandomTests(t, arrayTransactions, 42) await applyRandomTests(t, arrayTransactions, 42)
}) })

View File

@ -5,6 +5,7 @@ import yTest from './test-connector.js'
import Chance from 'chance' import Chance from 'chance'
import ItemJSON from '../src/Struct/ItemJSON.js' import ItemJSON from '../src/Struct/ItemJSON.js'
import ItemString from '../src/Struct/ItemString.js' import ItemString from '../src/Struct/ItemString.js'
import { defragmentItemContent } from '../src/Util/defragmentItemContent.js'
export const Y = _Y export const Y = _Y
@ -86,8 +87,12 @@ export async function compareUsers (t, users) {
await flushAll(t, users) await flushAll(t, users)
await wait() await wait()
await flushAll(t, users) 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 userMapValues = users.map(u => u.get('map', Y.Map).toJSON())
var userXmlValues = users.map(u => u.get('xml', Y.Xml).toString()) 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 => { var data = users.map(u => {
defragmentItemContent(u)
var data = {} var data = {}
let ops = [] let ops = []
u.os.iterate(null, null, function (op) { u.os.iterate(null, null, function (op) {
if (!op._deleted) { const json = {
const json = { id: op._id,
id: op._id, left: op._left === null ? null : op._left._id,
left: op._left === null ? null : op._left._id, right: op._right === null ? null : op._right._id,
right: op._right === null ? null : op._right._id, length: op._length,
length: op._length, deleted: op._deleted
deleted: op._deleted
}
if (op instanceof ItemJSON || op instanceof ItemString) {
json.content = op._content
}
ops.push(json)
} }
if (op instanceof ItemJSON || op instanceof ItemString) {
json.content = op._content
}
ops.push(json)
}) })
data.os = ops data.os = ops
data.ds = getDeleteSet(u) data.ds = getDeleteSet(u)
@ -173,6 +177,13 @@ export async function initArrays (t, opts) {
return attrs.filter(a => a !== 'hidden') 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) result.array0.delete(0, result.array0.length)
if (result.users[0].connector.testRoom != null) { 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 // TODO: We do not gc all users as this does not work yet
// await garbageCollectUsers(t, users) // await garbageCollectUsers(t, users)
await flushAll(t, users) await flushAll(t, users)
await users[0].db.emptyGarbageCollector() // await users[0].db.emptyGarbageCollector()
await flushAll(t, users) await flushAll(t, users)
} else if (chance.bool({likelihood: 10})) { } else if (chance.bool({likelihood: 10})) {
// 20%*!prev chance to flush some operations // 20%*!prev chance to flush some operations

View File

@ -1,6 +1,6 @@
/* global Y */ /* global Y */
import { wait } from './helper' import { wait } from './helper'
import { messageToString, messageToRoomname } from '../src/MessageHandler/messageToString' import { messageToString } from '../src/MessageHandler/messageToString'
var rooms = {} var rooms = {}
@ -14,8 +14,8 @@ export class TestRoom {
this.users.set(userID, connector) this.users.set(userID, connector)
for (let [uid, user] of this.users) { for (let [uid, user] of this.users) {
if (uid !== userID && (user.role === 'master' || connector.role === 'master')) { if (uid !== userID && (user.role === 'master' || connector.role === 'master')) {
connector.userJoined(uid, this.users.get(uid).role) connector.userJoined(uid, user.role)
this.users.get(uid).userJoined(userID, connector.role) user.userJoined(userID, connector.role)
} }
} }
} }
@ -38,13 +38,13 @@ export class TestRoom {
} }
async flushAll (users) { async flushAll (users) {
let flushing = true let flushing = true
let allUserIds = Array.from(this.users.keys()) let allUsers = Array.from(this.users.values())
if (users == null) { if (users == null) {
users = allUserIds.map(id => this.users.get(id).y) users = allUsers.map(user => user.y)
} }
while (flushing) { while (flushing) {
await wait(10) 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') flushing = res.some(status => status === 'flushing')
} }
} }
@ -83,13 +83,16 @@ export default function extendTestConnector (Y) {
for (let [user, conn] of this.connections) { for (let [user, conn] of this.connections) {
console.log(` ${user}:`) console.log(` ${user}:`)
for (let i = 0; i < conn.buffer.length; i++) { for (let i = 0; i < conn.buffer.length; i++) {
console.log(formatYjsMessage(conn.buffer[i])) console.log(messageToString(conn.buffer[i]))
} }
} }
} }
reconnect () { reconnect () {
this.testRoom.join(this) this.testRoom.join(this)
return super.reconnect() super.reconnect()
return new Promise(resolve => {
this.whenSynced(resolve)
})
} }
send (uid, message) { send (uid, message) {
super.send(uid, message) super.send(uid, message)
@ -116,20 +119,22 @@ export default function extendTestConnector (Y) {
}) })
} }
receiveMessage (sender, m) { receiveMessage (sender, m) {
if (this.y.userID !== sender && this.connections.has(sender)) { setTimeout(() => {
var buffer = this.connections.get(sender).buffer if (this.y.userID !== sender && this.connections.has(sender)) {
if (buffer == null) { var buffer = this.connections.get(sender).buffer
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) }, 0)
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)
})
}
}
} }
async _flushAll (flushUsers) { async _flushAll (flushUsers) {
if (flushUsers.some(u => u.connector.y.userID === this.y.userID)) { if (flushUsers.some(u => u.connector.y.userID === this.y.userID)) {