Compare commits

...

6 Commits

Author SHA1 Message Date
Kevin Jahns
1135c5e167 v13.0.0-13 -- distribution files 2017-08-13 01:05:06 +02:00
Kevin Jahns
86c46cf0ec 13.0.0-13 2017-08-13 01:04:37 +02:00
Kevin Jahns
8770c8e934 Implement persistence layer 2017-08-13 01:03:54 +02:00
Kevin Jahns
7e12ea2db5 move array tests and map tests to yjs 2017-08-09 02:21:17 +02:00
Kevin Jahns
3ca260e0da 13.0.0-12 2017-08-04 18:07:44 +02:00
Kevin Jahns
edb5e4f719 send sync step 1 after sync step 2 is processed (for slaves) 2017-08-04 18:06:36 +02:00
19 changed files with 26485 additions and 46 deletions

View File

@@ -1,5 +1,9 @@
/* global Y */
// eslint-disable-next-line
let search = new URLSearchParams(location.search)
let url = search.get('url')
// initialize a shared object. This function call returns a promise!
Y({
db: {
@@ -8,7 +12,7 @@ Y({
connector: {
name: 'websockets-client',
room: 'Textarea-example',
url: 'http://127.0.0.1:1234'
url: url || 'http://127.0.0.1:1234'
},
sourceDir: '/bower_components',
share: {

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "yjs",
"version": "13.0.0-7",
"version": "13.0.0-13",
"lockfileVersion": 1,
"dependencies": {
"accepts": {

View File

@@ -1,6 +1,6 @@
{
"name": "yjs",
"version": "13.0.0-11",
"version": "13.0.0-13",
"description": "A framework for real-time p2p shared editing on any data",
"main": "./y.node.js",
"browser": "./y.js",

View File

@@ -210,7 +210,8 @@ export default function extendConnector (Y/* :any */) {
/*
You received a raw message, and you know that it is intended for Yjs. Then call this function.
*/
receiveMessage (sender, buffer) {
receiveMessage (sender, buffer, skipAuth) {
skipAuth = skipAuth || false
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
return Promise.reject(new Error('Expected Message to be an ArrayBuffer or Uint8Array!'))
}
@@ -227,7 +228,7 @@ export default function extendConnector (Y/* :any */) {
this.log('%s: Receive \'%s\' from %s', this.userId, messageType, sender)
this.logMessage('Message: %Y', buffer)
if (senderConn == null) {
if (senderConn == null && !skipAuth) {
throw new Error('Received message from unknown peer!')
}
@@ -253,21 +254,21 @@ export default function extendConnector (Y/* :any */) {
})
}
}
if (senderConn.auth != null) {
return this.computeMessage(messageType, senderConn, decoder, encoder, sender)
if (skipAuth || senderConn.auth != null) {
return this.computeMessage(messageType, senderConn, decoder, encoder, sender, skipAuth)
} else {
senderConn.processAfterAuth.push([messageType, senderConn, decoder, encoder, sender])
senderConn.processAfterAuth.push([messageType, senderConn, decoder, encoder, sender, false])
}
}
computeMessage (messageType, senderConn, decoder, encoder, sender) {
computeMessage (messageType, senderConn, decoder, encoder, sender, skipAuth) {
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)
computeMessageSyncStep1(decoder, encoder, this, senderConn, sender)
return this.y.db.whenTransactionsFinished()
} else if (messageType === 'sync step 2' && senderConn.auth === 'write') {
return computeMessageSyncStep2(decoder, encoder, this, senderConn, sender)
} else if (messageType === 'update' && senderConn.auth === 'write') {
} else if (messageType === 'update' && (skipAuth || senderConn.auth === 'write')) {
return computeMessageUpdate(decoder, encoder, this, senderConn, sender)
} else {
return Promise.reject(new Error('Unable to receive message'))

View File

@@ -120,7 +120,7 @@ export default function extendDatabase (Y /* :any */) {
startGarbageCollector () {
this.gc = this.dbOpts.gc
if (this.gc) {
this.gcTimeout = !this.dbOpts.gcTimeout ? 100000 : this.dbOpts.gcTimeout
this.gcTimeout = !this.dbOpts.gcTimeout ? 30000 : this.dbOpts.gcTimeout
} else {
this.gcTimeout = -1
}

View File

@@ -7,37 +7,46 @@ export class BinaryEncoder {
constructor () {
this.data = []
}
get pos () {
return this.data.length
}
createBuffer () {
return Uint8Array.from(this.data).buffer
}
writeUint8 (num) {
this.data.push(num & bits8)
}
setUint8 (pos, num) {
this.data[pos] = num & bits8
}
writeUint16 (num) {
this.data.push(num & bits8, (num >>> 8) & bits8)
}
setUint16 (pos, num) {
this.data[pos] = num & bits8
this.data[pos + 1] = (num >>> 8) & bits8
}
writeUint32 (num) {
for (let i = 0; i < 4; i++) {
this.data.push(num & bits8)
num >>>= 8
}
}
setUint32 (pos, num) {
for (let i = 0; i < 4; i++) {
this.data[pos + i] = num & bits8
num >>>= 8
}
}
writeVarUint (num) {
while (num >= 0b10000000) {
this.data.push(0b10000000 | (bits7 & num))
@@ -45,6 +54,7 @@ export class BinaryEncoder {
}
this.data.push(bits7 & num)
}
writeVarString (str) {
let bytes = utf8.setBytesFromString(str)
let len = bytes.length
@@ -53,6 +63,7 @@ export class BinaryEncoder {
this.data.push(bytes[i])
}
}
writeOpID (id) {
let user = id[0]
this.writeVarUint(user)
@@ -68,19 +79,22 @@ export class BinaryDecoder {
constructor (buffer) {
if (buffer instanceof ArrayBuffer) {
this.uint8arr = new Uint8Array(buffer)
} else if (buffer instanceof Uint8Array) {
} else if (buffer instanceof Uint8Array || (typeof Buffer !== 'undefined' && buffer instanceof Buffer)) {
this.uint8arr = buffer
} else {
throw new Error('Expected an ArrayBuffer or Uint8Array!')
}
this.pos = 0
}
skip8 () {
this.pos++
}
readUint8 () {
return this.uint8arr[this.pos++]
}
readUint32 () {
let uint =
this.uint8arr[this.pos] +
@@ -90,9 +104,11 @@ export class BinaryDecoder {
this.pos += 4
return uint
}
peekUint8 () {
return this.uint8arr[this.pos]
}
readVarUint () {
let num = 0
let len = 0
@@ -108,6 +124,7 @@ export class BinaryDecoder {
}
}
}
readVarString () {
let len = this.readVarUint()
let bytes = new Array(len)
@@ -116,12 +133,14 @@ export class BinaryDecoder {
}
return utf8.getStringFromBytes(bytes)
}
peekVarString () {
let pos = this.pos
let s = this.readVarString()
this.pos = pos
return s
}
readOpID () {
let user = this.readVarUint()
if (user !== 0xFFFFFF) {

View File

@@ -34,7 +34,7 @@ export function logMessageUpdate (decoder, strBuilder) {
}
export function computeMessageUpdate (decoder, encoder, conn) {
if (conn.y.db.forwardAppliedOperations) {
if (conn.y.db.forwardAppliedOperations || conn.y.persistence != null) {
let messagePosition = decoder.pos
let len = decoder.readUint32()
let delops = []
@@ -45,7 +45,12 @@ export function computeMessageUpdate (decoder, encoder, conn) {
}
}
if (delops.length > 0) {
conn.broadcastOps(delops)
if (conn.y.db.forwardAppliedOperations) {
conn.broadcastOps(delops)
}
if (conn.y.persistence) {
conn.y.persistence.saveOperations(delops)
}
}
decoder.pos = messagePosition
}
@@ -92,27 +97,30 @@ export function computeMessageSyncStep1 (decoder, encoder, conn, senderConn, sen
conn.y.destroy()
}
// send sync step 2
conn.y.db.requestTransaction(function * () {
encoder.writeVarString('sync step 2')
encoder.writeVarString(conn.authInfo || '')
return conn.y.db.whenTransactionsFinished().then(() => {
// send sync step 2
conn.y.db.requestTransaction(function * () {
encoder.writeVarString('sync step 2')
encoder.writeVarString(conn.authInfo || '')
if (preferUntransformed) {
encoder.writeUint8(1)
yield * this.writeOperationsUntransformed(encoder)
} else {
encoder.writeUint8(0)
yield * this.writeOperations(encoder, decoder)
}
if (preferUntransformed) {
encoder.writeUint8(1)
yield * this.writeOperationsUntransformed(encoder)
} else {
encoder.writeUint8(0)
yield * this.writeOperations(encoder, decoder)
}
yield * this.writeDeleteSet(encoder)
conn.send(senderConn.uid, encoder.createBuffer())
senderConn.receivedSyncStep2 = true
yield * this.writeDeleteSet(encoder)
conn.send(senderConn.uid, encoder.createBuffer())
senderConn.receivedSyncStep2 = true
})
return conn.y.db.whenTransactionsFinished().then(() => {
if (conn.role === 'slave') {
sendSyncStep1(conn, sender)
}
})
})
if (conn.role === 'slave') {
sendSyncStep1(conn, sender)
}
return conn.y.db.whenTransactionsFinished()
}
export function logSS (decoder, strBuilder) {

43
src/Persistence.js Normal file
View File

@@ -0,0 +1,43 @@
import { BinaryEncoder } from './Encoding.js'
export default function extendPersistence (Y) {
class AbstractPersistence {
constructor (y, opts) {
this.y = y
this.opts = opts
this.saveOperationsBuffer = []
this.log = Y.debug('y:persistence')
}
saveToMessageQueue (binary) {
this.log('Room %s: Save message to message queue', this.y.options.connector.room)
}
saveOperations (ops) {
ops = ops.map(function (op) {
return Y.Struct[op.struct].encode(op)
})
const saveOperations = () => {
if (this.saveOperationsBuffer.length > 0) {
let encoder = new BinaryEncoder()
encoder.writeVarString(this.opts.room)
encoder.writeVarString('update')
let ops = this.saveOperationsBuffer
this.saveOperationsBuffer = []
let length = ops.length
encoder.writeUint32(length)
for (var i = 0; i < length; i++) {
let op = ops[i]
Y.Struct[op.struct].binaryEncode(encoder, op)
}
this.saveToMessageQueue(encoder.createBuffer())
}
}
if (this.saveOperationsBuffer.length === 0) {
this.saveOperationsBuffer = ops
this.y.db.whenTransactionsFinished().then(saveOperations)
} else {
this.saveOperationsBuffer = this.saveOperationsBuffer.concat(ops)
}
}
}
Y.AbstractPersistence = AbstractPersistence
}

View File

@@ -1,3 +1,4 @@
import { BinaryEncoder, BinaryDecoder } from './Encoding.js'
/*
Partial definition of a transaction
@@ -97,6 +98,9 @@ export default function extendTransaction (Y) {
if (send.length > 0) { // TODO: && !this.store.forwardAppliedOperations (but then i don't send delete ops)
// is connected, and this is not going to be send in addOperation
this.store.y.connector.broadcastOps(send)
if (this.store.y.persistence != null) {
this.store.y.persistence.saveOperations(send)
}
}
}
@@ -679,10 +683,15 @@ export default function extendTransaction (Y) {
yield * this.garbageCollectOperation(o.id)
}
}
if (this.store.forwardAppliedOperations) {
if (this.store.forwardAppliedOperations || this.store.y.persistence != null) {
var ops = []
ops.push({struct: 'Delete', target: [del[0], del[1]], length: del[2]})
this.store.y.connector.broadcastOps(ops)
if (this.store.forwardAppliedOperations) {
this.store.y.connector.broadcastOps(ops)
}
if (this.store.y.persistence != null) {
this.store.y.persistence.saveOperations(ops)
}
}
}
}
@@ -733,9 +742,14 @@ export default function extendTransaction (Y) {
* addOperation (op) {
yield * this.os.put(op)
// case op is created by this user, op is already broadcasted in applyCreatedOperations
if (op.id[0] !== this.store.userId && this.store.forwardAppliedOperations && typeof op.id[1] !== 'string') {
// is connected, and this is not going to be send in addOperation
this.store.y.connector.broadcastOps([op])
if (op.id[0] !== this.store.userId && typeof op.id[1] !== 'string') {
if (this.store.forwardAppliedOperations) {
// is connected, and this is not going to be send in addOperation
this.store.y.connector.broadcastOps([op])
}
if (this.store.y.persistence != null) {
this.store.y.persistence.saveOperations([op])
}
}
}
// if insertion, try to combine with left insertion (if both have content property)
@@ -1072,6 +1086,20 @@ export default function extendTransaction (Y) {
Y.Struct[op.struct].binaryEncode(encoder, Y.Struct[op.struct].encode(op))
}
}
* toBinary () {
let encoder = new BinaryEncoder()
yield * this.writeOperationsUntransformed(encoder)
yield * this.writeDeleteSet(encoder)
return encoder.createBuffer()
}
* fromBinary (buffer) {
let decoder = new BinaryDecoder(buffer)
yield * this.applyOperationsUntransformed(decoder)
yield * this.applyDeleteSet(decoder)
}
/*
* Get the plain untransformed operations from the database.
* You can apply these operations using .applyOperationsUntransformed(ops)

View File

@@ -1,4 +1,5 @@
import extendConnector from './Connector.js'
import extendPersistence from './Persistence.js'
import extendDatabase from './Database.js'
import extendTransaction from './Transaction.js'
import extendStruct from './Struct.js'
@@ -7,6 +8,7 @@ import debug from 'debug'
import { formatYjsMessage, formatYjsMessageType } from './MessageHandler.js'
extendConnector(Y)
extendPersistence(Y)
extendDatabase(Y)
extendTransaction(Y)
extendStruct(Y)
@@ -159,6 +161,11 @@ class YConfig extends Y.utils.NamedEventHandler {
this.options = opts
this.db = new Y[opts.db.name](this, opts.db)
this.connector = new Y[opts.connector.name](this, opts.connector)
if (opts.persistence != null) {
this.persistence = new Y[opts.persistence.name](this, opts.persistence)
} else {
this.persistence = null
}
this.connected = true
}
init (callback) {
@@ -188,9 +195,15 @@ class YConfig extends Y.utils.NamedEventHandler {
}
share[propertyname] = yield * this.store.initType.call(this, id, args)
}
this.store.whenTransactionsFinished()
.then(callback)
})
if (this.persistence != null) {
this.persistence.retrieveContent()
.then(() => this.db.whenTransactionsFinished())
.then(callback)
} else {
this.db.whenTransactionsFinished()
.then(callback)
}
}
isConnected () {
return this.connector.isSynced

374
test/y-array.tests.js Normal file
View File

@@ -0,0 +1,374 @@
import { wait, initArrays, compareUsers, Y, flushAll, garbageCollectUsers, applyRandomTests } from '../tests-lib/helper.js'
import { test, proxyConsole } from 'cutest'
proxyConsole()
test('basic spec', async function array0 (t) {
let { users, array0 } = await initArrays(t, { users: 2 })
array0.delete(0, 0)
t.assert(true, 'Does not throw when deleting zero elements with position 0')
let throwInvalidPosition = false
try {
array0.delete(1, 0)
} catch (e) {
throwInvalidPosition = true
}
t.assert(throwInvalidPosition, 'Throws when deleting zero elements with an invalid position')
array0.insert(0, ['A'])
array0.delete(1, 0)
t.assert(true, 'Does not throw when deleting zero elements with valid position 1')
await compareUsers(t, users)
})
test('insert three elements, try re-get property', async function array1 (t) {
var { users, array0, array1 } = await initArrays(t, { users: 2 })
array0.insert(0, [1, 2, 3])
t.compare(array0.toArray(), [1, 2, 3], '.toArray() works')
await flushAll(t, users)
t.compare(array1.toArray(), [1, 2, 3], '.toArray() works after sync')
await compareUsers(t, users)
})
test('concurrent insert (handle three conflicts)', async function array2 (t) {
var { users, array0, array1, array2 } = await initArrays(t, { users: 3 })
array0.insert(0, [0])
array1.insert(0, [1])
array2.insert(0, [2])
await compareUsers(t, users)
})
test('concurrent insert&delete (handle three conflicts)', async function array3 (t) {
var { users, array0, array1, array2 } = await initArrays(t, { users: 3 })
array0.insert(0, ['x', 'y', 'z'])
await flushAll(t, users)
array0.insert(1, [0])
array1.delete(0)
array1.delete(1, 1)
array2.insert(1, [2])
await compareUsers(t, users)
})
test('insertions work in late sync', async function array4 (t) {
var { users, array0, array1, array2 } = await initArrays(t, { users: 3 })
array0.insert(0, ['x', 'y'])
await flushAll(t, users)
users[1].disconnect()
users[2].disconnect()
array0.insert(1, ['user0'])
array1.insert(1, ['user1'])
array2.insert(1, ['user2'])
await users[1].reconnect()
await users[2].reconnect()
await compareUsers(t, users)
})
test('disconnect really prevents sending messages', async function array5 (t) {
var { users, array0, array1 } = await initArrays(t, { users: 3 })
array0.insert(0, ['x', 'y'])
await flushAll(t, users)
users[1].disconnect()
users[2].disconnect()
array0.insert(1, ['user0'])
array1.insert(1, ['user1'])
await wait(1000)
t.compare(array0.toArray(), ['x', 'user0', 'y'])
t.compare(array1.toArray(), ['x', 'user1', 'y'])
await users[1].reconnect()
await users[2].reconnect()
await compareUsers(t, users)
})
test('deletions in late sync', async function array6 (t) {
var { users, array0, array1 } = await initArrays(t, { users: 2 })
array0.insert(0, ['x', 'y'])
await flushAll(t, users)
await users[1].disconnect()
array1.delete(1, 1)
array0.delete(0, 2)
await wait()
await users[1].reconnect()
await compareUsers(t, users)
})
test('insert, then marge delete on sync', async function array7 (t) {
var { users, array0, array1 } = await initArrays(t, { users: 2 })
array0.insert(0, ['x', 'y', 'z'])
await flushAll(t, users)
await wait()
await users[0].disconnect()
array1.delete(0, 3)
await wait()
await users[0].reconnect()
await compareUsers(t, users)
})
function compareEvent (t, is, should) {
for (var key in should) {
t.assert(
should[key] === is[key] ||
JSON.stringify(should[key]) === JSON.stringify(is[key])
, 'event works as expected'
)
}
}
test('insert & delete events', async function array8 (t) {
var { array0, users } = await initArrays(t, { users: 2 })
var event
array0.observe(function (e) {
event = e
})
array0.insert(0, [0, 1, 2])
compareEvent(t, event, {
type: 'insert',
index: 0,
values: [0, 1, 2],
length: 3
})
array0.delete(0)
compareEvent(t, event, {
type: 'delete',
index: 0,
length: 1,
values: [0]
})
array0.delete(0, 2)
compareEvent(t, event, {
type: 'delete',
index: 0,
length: 2,
values: [1, 2]
})
await compareUsers(t, users)
})
test('insert & delete events for types', async function array9 (t) {
var { array0, users } = await initArrays(t, { users: 2 })
var event
array0.observe(function (e) {
event = e
})
array0.insert(0, [Y.Array])
compareEvent(t, event, {
type: 'insert',
object: array0,
index: 0,
length: 1
})
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
})
await compareUsers(t, users)
})
test('insert & delete events for types (2)', async function array10 (t) {
var { array0, users } = await initArrays(t, { users: 2 })
var events = []
array0.observe(function (e) {
events.push(e)
})
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
})
array0.delete(1)
compareEvent(t, events[2], {
type: 'delete',
object: array0,
index: 1,
length: 1
})
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()
array0.delete(0, 3)
await wait()
await users[0].reconnect()
await flushAll(t, users)
await garbageCollectUsers(t, users)
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 })
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.toArray()[0], '.toArray works as expected')
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 })
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.toArray()[0], '.toArray 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.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.toArray()[0], '.toArray 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.toArray()[0], '.toArray works as expected')
await compareUsers(t, users)
})
var _uniqueNumber = 0
function getUniqueNumber () {
return _uniqueNumber++
}
var arrayTransactions = [
function insert (t, user, chance) {
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)
},
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)
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)
map.set('someprop', 42)
map.set('someprop', 43)
map.set('someprop', 44)
},
function _delete (t, user, chance) {
var length = user.share.array._content.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) {
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')
}
} else {
user.share.array.delete(pos, delLength)
}
} else {
user.share.array.delete(pos, delLength)
}
}
}
]
test('y-array: Random tests (42)', async function randomArray42 (t) {
await applyRandomTests(t, arrayTransactions, 42)
})
test('y-array: Random tests (43)', async function randomArray43 (t) {
await applyRandomTests(t, arrayTransactions, 43)
})
test('y-array: Random tests (44)', async function randomArray44 (t) {
await applyRandomTests(t, arrayTransactions, 44)
})
test('y-array: Random tests (45)', async function randomArray45 (t) {
await applyRandomTests(t, arrayTransactions, 45)
})
test('y-array: Random tests (46)', async function randomArray46 (t) {
await applyRandomTests(t, arrayTransactions, 46)
})
test('y-array: Random tests (47)', async function randomArray47 (t) {
await applyRandomTests(t, arrayTransactions, 47)
})
/*
test('y-array: Random tests (200)', async function randomArray200 (t) {
await applyRandomTests(t, arrayTransactions, 200)
})
test('y-array: Random tests (300)', async function randomArray300 (t) {
await applyRandomTests(t, arrayTransactions, 300)
})
test('y-array: Random tests (400)', async function randomArray400 (t) {
await applyRandomTests(t, arrayTransactions, 400)
})
test('y-array: Random tests (500)', async function randomArray500 (t) {
await applyRandomTests(t, arrayTransactions, 500)
})
*/

371
test/y-map.tests.js Normal file
View File

@@ -0,0 +1,371 @@
import { initArrays, compareUsers, Y, flushAll, applyRandomTests } from '../tests-lib/helper.js'
import { test, proxyConsole } from 'cutest'
proxyConsole()
test('basic map tests', async function map0 (t) {
let { users, map0, map1, map2 } = await initArrays(t, { users: 3 })
users[2].disconnect()
map0.set('number', 1)
map0.set('string', 'hello Y')
map0.set('object', { key: { key2: 'value' } })
map0.set('y-map', Y.Map)
let map = map0.get('y-map')
map.set('y-array', Y.Array)
let array = map.get('y-array')
array.insert(0, [0])
array.insert(0, [-1])
t.assert(map0.get('number') === 1, 'client 0 computed the change (number)')
t.assert(map0.get('string') === 'hello Y', 'client 0 computed the change (string)')
t.compare(map0.get('object'), { key: { key2: 'value' } }, 'client 0 computed the change (object)')
t.assert(map0.get('y-map').get('y-array').get(0) === -1, 'client 0 computed the change (type)')
await users[2].reconnect()
await flushAll(t, users)
t.assert(map1.get('number') === 1, 'client 1 received the update (number)')
t.assert(map1.get('string') === 'hello Y', 'client 1 received the update (string)')
t.compare(map1.get('object'), { key: { key2: 'value' } }, 'client 1 received the update (object)')
t.assert(map1.get('y-map').get('y-array').get(0) === -1, 'client 1 received the update (type)')
// compare disconnected user
t.assert(map2.get('number') === 1, 'client 2 received the update (number) - was disconnected')
t.assert(map2.get('string') === 'hello Y', 'client 2 received the update (string) - was disconnected')
t.compare(map2.get('object'), { key: { key2: 'value' } }, 'client 2 received the update (object) - was disconnected')
t.assert(map2.get('y-map').get('y-array').get(0) === -1, 'client 2 received the update (type) - was disconnected')
await compareUsers(t, users)
})
test('Basic get&set of Map property (converge via sync)', async function map1 (t) {
let { users, map0 } = await initArrays(t, { users: 2 })
map0.set('stuff', 'stuffy')
t.compare(map0.get('stuff'), 'stuffy')
await flushAll(t, users)
for (let user of users) {
var u = user.share.map
t.compare(u.get('stuff'), 'stuffy')
}
await compareUsers(t, users)
})
test('Map can set custom types (Map)', async function map2 (t) {
let { users, map0 } = await initArrays(t, { users: 2 })
var map = map0.set('Map', Y.Map)
map.set('one', 1)
map = map0.get('Map')
t.compare(map.get('one'), 1)
await compareUsers(t, users)
})
test('Map can set custom types (Map) - get also returns the type', async function map3 (t) {
let { users, map0 } = await initArrays(t, { users: 2 })
map0.set('Map', Y.Map)
var map = map0.get('Map')
map.set('one', 1)
map = map0.get('Map')
t.compare(map.get('one'), 1)
await compareUsers(t, users)
})
test('Map can set custom types (Array)', async function map4 (t) {
let { users, map0 } = await initArrays(t, { users: 2 })
var array = map0.set('Array', Y.Array)
array.insert(0, [1, 2, 3])
array = map0.get('Array')
t.compare(array.toArray(), [1, 2, 3])
await compareUsers(t, users)
})
test('Basic get&set of Map property (converge via update)', async function map5 (t) {
let { users, map0 } = await initArrays(t, { users: 2 })
map0.set('stuff', 'stuffy')
t.compare(map0.get('stuff'), 'stuffy')
await flushAll(t, users)
for (let user of users) {
var u = user.share.map
t.compare(u.get('stuff'), 'stuffy')
}
await compareUsers(t, users)
})
test('Basic get&set of Map property (handle conflict)', async function map6 (t) {
let { users, map0, map1 } = await initArrays(t, { users: 3 })
map0.set('stuff', 'c0')
map1.set('stuff', 'c1')
await flushAll(t, users)
for (let user of users) {
var u = user.share.map
t.compare(u.get('stuff'), 'c0')
}
await compareUsers(t, users)
})
test('Basic get&set&delete of Map property (handle conflict)', async function map7 (t) {
let { users, map0, map1 } = await initArrays(t, { users: 3 })
map0.set('stuff', 'c0')
map0.delete('stuff')
map1.set('stuff', 'c1')
await flushAll(t, users)
for (let user of users) {
var u = user.share.map
t.assert(u.get('stuff') === undefined)
}
await compareUsers(t, users)
})
test('Basic get&set of Map property (handle three conflicts)', async function map8 (t) {
let { users, map0, map1, map2 } = await initArrays(t, { users: 3 })
map0.set('stuff', 'c0')
map1.set('stuff', 'c1')
map1.set('stuff', 'c2')
map2.set('stuff', 'c3')
await flushAll(t, users)
for (let user of users) {
var u = user.share.map
t.compare(u.get('stuff'), 'c0')
}
await compareUsers(t, users)
})
test('Basic get&set&delete of Map property (handle three conflicts)', async function map9 (t) {
let { users, map0, map1, map2, map3 } = await initArrays(t, { users: 4 })
map0.set('stuff', 'c0')
map1.set('stuff', 'c1')
map1.set('stuff', 'c2')
map2.set('stuff', 'c3')
await flushAll(t, users)
map0.set('stuff', 'deleteme')
map0.delete('stuff')
map1.set('stuff', 'c1')
map2.set('stuff', 'c2')
map3.set('stuff', 'c3')
await flushAll(t, users)
for (let user of users) {
var u = user.share.map
t.assert(u.get('stuff') === undefined)
}
await compareUsers(t, users)
})
test('observePath properties', async function map10 (t) {
let { users, map0, map1, map2 } = await initArrays(t, { users: 3 })
let map
map0.observePath(['map'], function (map) {
if (map != null) {
map.set('yay', 4)
}
})
map1.set('map', Y.Map)
await flushAll(t, users)
map = map2.get('map')
t.compare(map.get('yay'), 4)
await compareUsers(t, users)
})
test('observe deep properties', async function map11 (t) {
let { users, map1, map2, map3 } = await initArrays(t, { users: 4 })
var _map1 = map1.set('map', Y.Map)
var calls = 0
var dmapid
_map1.observe(function (event) {
calls++
t.compare(event.name, 'deepmap')
dmapid = event.object.opContents.deepmap
})
await flushAll(t, users)
var _map3 = map3.get('map')
_map3.set('deepmap', Y.Map)
await flushAll(t, users)
var _map2 = map2.get('map')
_map2.set('deepmap', Y.Map)
await flushAll(t, users)
var dmap1 = _map1.get('deepmap')
var dmap2 = _map2.get('deepmap')
var dmap3 = _map3.get('deepmap')
t.assert(calls > 0)
t.compare(dmap1._model, dmap2._model)
t.compare(dmap1._model, dmap3._model)
t.compare(dmap1._model, dmapid)
await compareUsers(t, users)
})
test('observes using observePath', async function map12 (t) {
let { users, map0 } = await initArrays(t, { users: 2 })
var pathes = []
var calls = 0
map0.observeDeep(function (event) {
pathes.push(event.path)
calls++
})
map0.set('map', Y.Map)
map0.get('map').set('array', Y.Array)
map0.get('map').get('array').insert(0, ['content'])
t.assert(calls === 3)
t.compare(pathes, [[], ['map'], ['map', 'array']])
await compareUsers(t, users)
})
function compareEvent (t, is, should) {
for (var key in should) {
t.assert(should[key] === is[key])
}
}
test('throws add & update & delete events (with type and primitive content)', async function map13 (t) {
let { users, map0 } = await initArrays(t, { users: 2 })
var event
await flushAll(t, users)
map0.observe(function (e) {
event = e // just put it on event, should be thrown synchronously anyway
})
map0.set('stuff', 4)
compareEvent(t, event, {
type: 'add',
object: map0,
name: 'stuff'
})
// update, oldValue is in contents
map0.set('stuff', Y.Array)
compareEvent(t, event, {
type: 'update',
object: map0,
name: 'stuff',
oldValue: 4
})
var replacedArray = map0.get('stuff')
// update, oldValue is in opContents
map0.set('stuff', 5)
var array = event.oldValue
t.compare(array._model, replacedArray._model)
// delete
map0.delete('stuff')
compareEvent(t, event, {
type: 'delete',
name: 'stuff',
object: map0,
oldValue: 5
})
await compareUsers(t, users)
})
test('event has correct value when setting a primitive on a YMap (same user)', async function map14 (t) {
let { users, map0 } = await initArrays(t, { users: 3 })
var event
await flushAll(t, users)
map0.observe(function (e) {
event = e
})
map0.set('stuff', 2)
t.compare(event.value, event.object.get(event.name))
await compareUsers(t, users)
})
test('event has correct value when setting a primitive on a YMap (received from another user)', async function map15 (t) {
let { users, map0, map1 } = await initArrays(t, { users: 3 })
var event
await flushAll(t, users)
map0.observe(function (e) {
event = e
})
map1.set('stuff', 2)
await flushAll(t, users)
t.compare(event.value, event.object.get(event.name))
await compareUsers(t, users)
})
test('event has correct value when setting a type on a YMap (same user)', async function map16 (t) {
let { users, map0 } = await initArrays(t, { users: 3 })
var event
await flushAll(t, users)
map0.observe(function (e) {
event = e
})
map0.set('stuff', Y.Map)
t.compare(event.value._model, event.object.get(event.name)._model)
await compareUsers(t, users)
})
test('event has correct value when setting a type on a YMap (ops received from another user)', async function map17 (t) {
let { users, map0, map1 } = await initArrays(t, { users: 3 })
var event
await flushAll(t, users)
map0.observe(function (e) {
event = e
})
map1.set('stuff', Y.Map)
await flushAll(t, users)
t.compare(event.value._model, event.object.get(event.name)._model)
await compareUsers(t, users)
})
var mapTransactions = [
function set (t, user, chance) {
let key = chance.pickone(['one', 'two'])
var value = chance.string()
user.share.map.set(key, value)
},
function setType (t, user, chance) {
let key = chance.pickone(['one', 'two'])
var value = chance.pickone([Y.Array, Y.Map])
let type = user.share.map.set(key, value)
if (value === Y.Array) {
type.insert(0, [1, 2, 3, 4])
} else {
type.set('deepkey', 'deepvalue')
}
},
function _delete (t, user, chance) {
let key = chance.pickone(['one', 'two'])
user.share.map.delete(key)
}
]
test('y-map: Random tests (42)', async function randomMap42 (t) {
await applyRandomTests(t, mapTransactions, 42)
})
test('y-map: Random tests (43)', async function randomMap43 (t) {
await applyRandomTests(t, mapTransactions, 43)
})
test('y-map: Random tests (44)', async function randomMap44 (t) {
await applyRandomTests(t, mapTransactions, 44)
})
test('y-map: Random tests (45)', async function randomMap45 (t) {
await applyRandomTests(t, mapTransactions, 45)
})
test('y-map: Random tests (46)', async function randomMap46 (t) {
await applyRandomTests(t, mapTransactions, 46)
})
test('y-map: Random tests (47)', async function randomMap47 (t) {
await applyRandomTests(t, mapTransactions, 47)
})
/*
test('y-map: Random tests (200)', async function randomMap200 (t) {
await applyRandomTests(t, mapTransactions, 200)
})
test('y-map: Random tests (300)', async function randomMap300 (t) {
await applyRandomTests(t, mapTransactions, 300)
})
test('y-map: Random tests (400)', async function randomMap400 (t) {
await applyRandomTests(t, mapTransactions, 400)
})
test('y-map: Random tests (500)', async function randomMap500 (t) {
await applyRandomTests(t, mapTransactions, 500)
})
*/

View File

@@ -12,6 +12,9 @@ export let Y = _Y
Y.extend(yMemory, yArray, yMap, yTest)
export var database = { name: 'memory' }
export var connector = { name: 'test', url: 'http://localhost:1234' }
function * getStateSet () {
var ss = {}
yield * this.ss.iterate(this, null, null, function * (n) {
@@ -60,7 +63,16 @@ export async function compareUsers (t, users) {
await wait()
await flushAll(t, users)
var userTypeContents = users.map(u => u.share.array._content.map(c => c.val || JSON.stringify(c.type)))
var userArrayValues = users.map(u => u.share.array._content.map(c => c.val || JSON.stringify(c.type)))
function valueToComparable (v) {
if (v != null && v._model != null) {
return v._model
} else {
return v || null
}
}
var userMapOneValues = users.map(u => u.share.map.get('one')).map(valueToComparable)
var userMapTwoValues = users.map(u => u.share.map.get('two')).map(valueToComparable)
await users[0].db.garbageCollect()
await users[0].db.garbageCollect()
@@ -118,7 +130,9 @@ export async function compareUsers (t, users) {
}))
for (var i = 0; i < data.length - 1; i++) {
await t.asyncGroup(async () => {
t.compare(userTypeContents[i], userTypeContents[i + 1], 'types')
t.compare(userArrayValues[i], userArrayValues[i + 1], 'array types')
t.compare(userMapOneValues[i], userMapOneValues[i + 1], 'map types (propery "one")')
t.compare(userMapTwoValues[i], userMapTwoValues[i + 1], 'map types (propery "two")')
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')
@@ -135,17 +149,17 @@ export async function initArrays (t, opts) {
}
var share = Object.assign({ flushHelper: 'Map', array: 'Array', map: 'Map' }, opts.share)
var chance = opts.chance || new Chance(t.getSeed() * 1000000000)
var connector = Object.assign({ room: 'debugging_' + t.name, generateUserId: false, testContext: t, chance }, opts.connector)
var conn = Object.assign({ room: 'debugging_' + t.name, generateUserId: false, testContext: t, chance }, connector)
for (let i = 0; i < opts.users; i++) {
let dbOpts
let connOpts
if (i === 0) {
// Only one instance can gc!
dbOpts = Object.assign({ gc: false }, opts.db)
connOpts = Object.assign({ role: 'master' }, connector)
dbOpts = Object.assign({ gc: false }, database)
connOpts = Object.assign({ role: 'master' }, conn)
} else {
dbOpts = Object.assign({ gc: false }, opts.db)
connOpts = Object.assign({ role: 'slave' }, connector)
dbOpts = Object.assign({ gc: false }, database)
connOpts = Object.assign({ role: 'slave' }, conn)
}
let y = await Y({
connector: connOpts,
@@ -220,3 +234,48 @@ export function wait (t) {
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
}

10
y.js Normal file

File diff suppressed because one or more lines are too long

1
y.js.map Normal file

File diff suppressed because one or more lines are too long

5052
y.node.js Normal file

File diff suppressed because it is too large Load Diff

1
y.node.js.map Normal file

File diff suppressed because one or more lines are too long

20454
y.test.js Normal file

File diff suppressed because one or more lines are too long

1
y.test.js.map Normal file

File diff suppressed because one or more lines are too long