275 lines
8.3 KiB
JavaScript
275 lines
8.3 KiB
JavaScript
|
|
import _Y from '../src/Y.dist.js'
|
|
import { DomBinding } from '../src/Y.js'
|
|
import TestConnector 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'
|
|
import Quill from 'quill'
|
|
import GC from '../src/Struct/GC.js'
|
|
|
|
export const Y = _Y
|
|
|
|
export const database = { name: 'memory' }
|
|
export const connector = { name: 'test', url: 'http://localhost:1234' }
|
|
|
|
Y.test = TestConnector
|
|
|
|
function getStateSet (y) {
|
|
let ss = {}
|
|
for (let [user, clock] of y.ss.state) {
|
|
ss[user] = clock
|
|
}
|
|
return ss
|
|
}
|
|
|
|
function getDeleteSet (y) {
|
|
var ds = {}
|
|
y.ds.iterate(null, null, function (n) {
|
|
var user = n._id.user
|
|
var counter = n._id.clock
|
|
var len = n.len
|
|
var gc = n.gc
|
|
var dv = ds[user]
|
|
if (dv === void 0) {
|
|
dv = []
|
|
ds[user] = dv
|
|
}
|
|
dv.push([counter, len, gc])
|
|
})
|
|
return ds
|
|
}
|
|
|
|
/*
|
|
* 1. reconnect and flush all
|
|
* 2. user 0 gc
|
|
* 3. get type content
|
|
* 4. disconnect & reconnect all (so gc is propagated)
|
|
* 5. compare os, ds, ss
|
|
*/
|
|
export async function compareUsers (t, users) {
|
|
await Promise.all(users.map(u => u.reconnect()))
|
|
if (users[0].connector.testRoom == null) {
|
|
await wait(100)
|
|
}
|
|
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.define('array', Y.Array).toJSON().map(val => JSON.stringify(val)))
|
|
var userMapValues = users.map(u => u.define('map', Y.Map).toJSON())
|
|
var userXmlValues = users.map(u => u.define('xml', Y.Xml).toString())
|
|
var userTextValues = users.map(u => u.define('text', Y.Text).toDelta())
|
|
var userQuillValues = users.map(u => {
|
|
u.quill.update('yjs') // get latest changes
|
|
return u.quill.getContents().ops
|
|
})
|
|
|
|
var data = users.map(u => {
|
|
defragmentItemContent(u)
|
|
var data = {}
|
|
let ops = []
|
|
u.os.iterate(null, null, function (op) {
|
|
let json
|
|
if (op.constructor === GC) {
|
|
json = {
|
|
type: 'GC',
|
|
id: op._id,
|
|
length: op._length
|
|
}
|
|
} else {
|
|
json = {
|
|
id: op._id,
|
|
left: op._left === null ? null : op._left._lastId,
|
|
right: op._right === null ? null : op._right._id,
|
|
length: op._length,
|
|
deleted: op._deleted,
|
|
parent: op._parent._id
|
|
}
|
|
}
|
|
if (op instanceof ItemJSON || op instanceof ItemString) {
|
|
json.content = op._content
|
|
}
|
|
ops.push(json)
|
|
})
|
|
data.os = ops
|
|
data.ds = getDeleteSet(u)
|
|
data.ss = getStateSet(u)
|
|
return data
|
|
})
|
|
for (var i = 0; i < data.length - 1; i++) {
|
|
await t.asyncGroup(async () => {
|
|
t.compare(userArrayValues[i], userArrayValues[i + 1], 'array types')
|
|
t.compare(userMapValues[i], userMapValues[i + 1], 'map types')
|
|
t.compare(userXmlValues[i], userXmlValues[i + 1], 'xml types')
|
|
t.compare(userTextValues[i], userTextValues[i + 1], 'text types')
|
|
t.compare(userQuillValues[i], userQuillValues[i + 1], 'quill delta content')
|
|
t.compare(data[i].os, data[i + 1].os, 'os')
|
|
t.compare(data[i].ds, data[i + 1].ds, 'ds')
|
|
t.compare(data[i].ss, data[i + 1].ss, 'ss')
|
|
}, `Compare user${i} with user${i + 1}`)
|
|
}
|
|
users.map(u => u.destroy())
|
|
}
|
|
|
|
function domFilter (nodeName, attrs) {
|
|
if (nodeName === 'HIDDEN') {
|
|
return null
|
|
}
|
|
attrs.delete('hidden')
|
|
return attrs
|
|
}
|
|
|
|
export async function initArrays (t, opts) {
|
|
var result = {
|
|
users: []
|
|
}
|
|
var chance = opts.chance || new Chance(t.getSeed() * 1000000000)
|
|
var conn = Object.assign({ room: 'debugging_' + t.name, testContext: t, chance }, connector)
|
|
for (let i = 0; i < opts.users; i++) {
|
|
let connOpts
|
|
if (i === 0) {
|
|
connOpts = Object.assign({ role: 'master' }, conn)
|
|
} else {
|
|
connOpts = Object.assign({ role: 'slave' }, conn)
|
|
}
|
|
let y = new Y(connOpts.room, {
|
|
userID: i, // evil hackery, don't try this at home
|
|
connector: connOpts
|
|
})
|
|
result.users.push(y)
|
|
result['array' + i] = y.define('array', Y.Array)
|
|
result['map' + i] = y.define('map', Y.Map)
|
|
const yxml = y.define('xml', Y.XmlElement)
|
|
result['xml' + i] = yxml
|
|
const dom = document.createElement('my-dom')
|
|
const domBinding = new DomBinding(yxml, dom, { domFilter })
|
|
result['domBinding' + i] = domBinding
|
|
result['dom' + i] = dom
|
|
const textType = y.define('text', Y.Text)
|
|
result['text' + i] = textType
|
|
const quill = new Quill(document.createElement('div'))
|
|
result['quillBinding' + i] = new Y.QuillBinding(textType, quill)
|
|
result['quill' + i] = quill
|
|
y.quill = quill // put quill on the y object (so we can use it later)
|
|
y.dom = dom
|
|
y.on('afterTransaction', function () {
|
|
for (let missing of y._missingStructs.values()) {
|
|
if (missing.size > 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) {
|
|
// flush for sync if test-connector
|
|
await result.users[0].connector.testRoom.flushAll(result.users)
|
|
}
|
|
await Promise.all(result.users.map(u => {
|
|
return new Promise(function (resolve) {
|
|
u.connector.whenSynced(resolve)
|
|
})
|
|
}))
|
|
await flushAll(t, result.users)
|
|
return result
|
|
}
|
|
|
|
export async function flushAll (t, users) {
|
|
// users = users.filter(u => u.connector.isSynced)
|
|
if (users.length === 0) {
|
|
return
|
|
}
|
|
await wait(10)
|
|
if (users[0].connector.testRoom != null) {
|
|
// use flushAll method specified in Test Connector
|
|
await users[0].connector.testRoom.flushAll(users)
|
|
} else {
|
|
var flushCounter = users[0].get('flushHelper', Y.Map).get('0') || 0
|
|
flushCounter++
|
|
await Promise.all(users.map(async (u, i) => {
|
|
// wait for all users to set the flush counter to the same value
|
|
await new Promise(resolve => {
|
|
function observer () {
|
|
var allUsersReceivedUpdate = true
|
|
for (var i = 0; i < users.length; i++) {
|
|
if (u.get('flushHelper', Y.Map).get(i + '') !== flushCounter) {
|
|
allUsersReceivedUpdate = false
|
|
break
|
|
}
|
|
}
|
|
if (allUsersReceivedUpdate) {
|
|
resolve()
|
|
}
|
|
}
|
|
u.get('flushHelper', Y.Map).observe(observer)
|
|
u.get('flushHelper').set(i + '', flushCounter)
|
|
})
|
|
}))
|
|
}
|
|
}
|
|
|
|
export async function flushSome (t, users) {
|
|
if (users[0].connector.testRoom == null) {
|
|
// if not test-connector, wait for some time for operations to arrive
|
|
await wait(100)
|
|
}
|
|
}
|
|
|
|
export function wait (t) {
|
|
return new Promise(function (resolve) {
|
|
setTimeout(resolve, t != null ? t : 100)
|
|
})
|
|
}
|
|
|
|
export async function applyRandomTests (t, mods, iterations) {
|
|
const chance = new Chance(t.getSeed() * 1000000000)
|
|
var initInformation = await initArrays(t, { users: 5, chance: chance })
|
|
let { users } = initInformation
|
|
for (var i = 0; i < iterations; i++) {
|
|
if (chance.bool({likelihood: 10})) {
|
|
// 10% chance to disconnect/reconnect a user
|
|
// we make sure that the first users always is connected
|
|
let user = chance.pickone(users.slice(1))
|
|
if (user.connector.isSynced) {
|
|
if (users.filter(u => u.connector.isSynced).length > 1) {
|
|
// make sure that at least one user remains in the room
|
|
await user.disconnect()
|
|
if (users[0].connector.testRoom == null) {
|
|
await wait(100)
|
|
}
|
|
}
|
|
} else {
|
|
await user.reconnect()
|
|
if (users[0].connector.testRoom == null) {
|
|
await wait(100)
|
|
}
|
|
await new Promise(function (resolve) {
|
|
user.connector.whenSynced(resolve)
|
|
})
|
|
}
|
|
} else if (chance.bool({likelihood: 5})) {
|
|
// 20%*!prev chance to flush all & garbagecollect
|
|
// 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 flushAll(t, users)
|
|
} else if (chance.bool({likelihood: 10})) {
|
|
// 20%*!prev chance to flush some operations
|
|
await flushSome(t, users)
|
|
}
|
|
let user = chance.pickone(users)
|
|
var test = chance.pickone(mods)
|
|
test(t, user, chance)
|
|
}
|
|
await compareUsers(t, users)
|
|
return initInformation
|
|
}
|